| #!/usr/bin/env python3 |
| """Script to generate BUILD.gn files from Abseil's BUILD.bazel at roll time.""" |
| |
| import ast |
| import datetime |
| import logging |
| import os |
| import re |
| import subprocess |
| import sys |
| |
| # List of targets that do not have matching gn target generated. |
| _SKIP_TARGETS = { |
| 'container:btree_test': |
| 'TODO(mbonadei): Fix issue with EXPECT_DEATH and uncomment.', |
| 'container:container_memory_test': |
| 'Disabled because container_memory_test requires -frtti', |
| 'container:raw_hash_set_test': |
| 'raw_hash_set_test uses typeid(), i.e., relies on RTTI.', |
| 'debugging:demangle_test': 'Disabled because this test relies on RTTI', |
| 'flags:commandlineflag_test': |
| 'Conflicts at link_time with "parse_test" because defines the same flags.', |
| 'flags:reflection_test': |
| 'Conflicts at link time with "flag_test" because defines the same flags.', |
| 'log:check_test': 'Overlaps with absl_check_test', |
| 'log:log_basic_test': 'Overlaps with absl_log_basic_test', |
| 'profiling:sample_recorder_test': |
| 'TODO: Re-enable once the issue with gmock activating generic gtest printers hitting issues with -Wmicrosoft-cast.', |
| 'random/internal:nanobenchmark': 'Only used for benchmarking', |
| 'synchronization:blocking_counter_test': |
| 'Conflicts at link time with "tracing_strong_test" because also defines strong functions for AbslInternalTraceWait and alike', |
| 'synchronization:mutex_method_pointer_test': 'Doesn\'t compile.', |
| 'synchronization:notification_test': |
| 'Conflicts at link time with "tracing_strong_test" because also defines strong functions for AbslInternalTraceWait and alike', |
| 'types:any_span_test': |
| 'any_span_test is not ported because relies on RTTI', |
| } |
| |
| # Targets that are public in absl in general, but are not exposed to chromium targets |
| _PRIVATE_TARGETS = { |
| # Flags rely on static initializers and thus are not exposed in chromium |
| 'flags:config', |
| 'flags:commandlineflag', |
| 'flags:flag', |
| 'flags:marshalling', |
| 'flags:parse', |
| 'flags:reflection', |
| 'flags:usage', |
| 'log:flags', |
| # Targets below expose macros that conflicts with similar chromium macros, |
| # there are targets with alternative absl macro prefixed with ABSL_. |
| 'log:log', |
| 'log:check', |
| 'log:vlog_is_on', |
| # absl::any, same as std::any doesn't work with chromium component builds. |
| 'types:any', |
| # currently public, but shouldn't be, there are TODOs to make them private |
| 'base:malloc_internal', |
| 'cleanup:cleanup_internal', |
| 'container:compressed_tuple', |
| 'container:raw_hash_set', |
| 'strings:internal', |
| # public in cctz, private for abseil users. |
| 'time/internal/cctz:civil_time', |
| 'time/internal/cctz:time_zone', |
| } |
| |
| # Dependencies that preferably shouldn't be public in chromium, but are. |
| _PUBLIC_TARGETS = { |
| 'base:dynamic_annotations', |
| 'base:raw_logging_internal', |
| 'container:layout', |
| } |
| |
| # Extra output added just before the target. |
| _ADD_PREFIX = { |
| 'base:c_header_test': |
| 'if (absl_build_tests) { import("//testing/test.gni") ', |
| 'flags:config': |
| '''# Since absl/flags are only used by some test binaries (e.g. in WebRTC), |
| # there is no need to strip flags from mobile platforms binaries. |
| # This does not affect Chromium. |
| config("absl_flags_config") { |
| defines = [ "ABSL_FLAGS_STRIP_NAMES=0" ] |
| } |
| ''', |
| 'log:check': |
| '''# This target is banned both for 1st party and 3rd party code, use absl_check |
| # in third_party code instead. |
| # This header introduces CHECK macros that collides with chromium's macros. |
| # Instead libraries should use ABSL_CHECK that provides the same functionality.''', |
| 'log:log': |
| '''# This target is banned both for 1st party and 3rd party code. Use absl_log |
| # in third_party code instead. |
| # This header introduces LOG macros that collides with chromium's macros. |
| # Instead libraries should use ABSL_LOG that provides the same functionality.''', |
| 'log:vlog_is_on': |
| '''# This target is banned both for 1st party and 3rd party code, use ABSL_ |
| # prefixed macros and absl_vlog_is_on in third_party code instead.''' |
| } |
| |
| # Extra build rules added at the end. The reason they are needed vary per target. |
| _ADD_CONTENT = { |
| 'base:c_header_test': |
| '}', # Closes extra '{' opened by prefix. |
| 'cleanup:cleanup_internal': |
| 'visibility = [ "//third_party/abseil-cpp/absl/*" ]', |
| 'container:hashtablez_sampler_test': |
| 'if (is_win) { sources = [] }', |
| 'container:test_allocator': |
| 'deps = [ "//third_party/abseil-cpp/absl/base:config", "//third_party/googletest:gtest" ]', |
| 'flags:config': |
| 'public_configs = [ ":absl_flags_config" ]', |
| 'flags:parse_test': |
| 'if (is_ios) { sources = [] }', |
| 'log:absl_check_test': |
| 'if (is_ios) { sources = [] }', |
| 'log:log_sink_test': |
| 'if (is_ios) { sources = [] }', |
| 'log:scoped_mock_log_test': |
| 'if (is_ios) { sources = [] }', |
| 'log/internal:log_sink_set': |
| 'if (is_android) { libs = [ "log" ] }', |
| 'log/internal:stderr_log_sink_test': |
| 'if (is_apple || is_android) { sources = [] }', |
| 'random:uniform_real_distribution_test': |
| 'if (is_ios) { sources = [] }', |
| 'random/internal:seed_material': |
| 'if (is_win) { libs = [ "bcrypt.lib" ]}', |
| 'strings:strings': |
| '''public_deps = [ |
| # string_view.h was once part of :strings, so string_view.h is |
| # re-exported for backwards compatibility. |
| # New code should directly depend on :string_view. |
| # TODO(crbug.com/40276308): Remove once all targets are migrated to |
| # :string_view. |
| ":string_view" ]''', |
| 'strings:str_format_convert_test': |
| 'if (is_fuchsia) { sources = [] }', |
| # Instead of parsing 'select', add platform-specific rules manually while there are few of those. |
| 'time/internal/cctz:time_zone': |
| ''' |
| if (is_win) { sources += [ "src/time_zone_name_win.cc" ] |
| public += [ "src/time_zone_name_win.h" ] } |
| defines = [] |
| if (is_apple) { |
| frameworks = [ "Foundation.framework" ] |
| # Work-around for https://github.com/llvm/llvm-project/issues/117630 |
| defines += [ "_XOPEN_SOURCE=700" ] |
| } |
| if (is_fuchsia) { |
| deps += [ |
| "//third_party/fuchsia-sdk/sdk/fidl/fuchsia.intl:fuchsia.intl_hlcpp", |
| "//third_party/fuchsia-sdk/sdk/pkg/async", |
| "//third_party/fuchsia-sdk/sdk/pkg/async-loop-cpp", |
| "//third_party/fuchsia-sdk/sdk/pkg/sys_cpp", |
| "//third_party/fuchsia-sdk/sdk/pkg/zx", |
| ] |
| }''', |
| 'time/internal/cctz:time_zone_name_win_test': |
| 'if (is_win) { sources = [ "src/time_zone_name_win_test.cc" ] }', |
| } |
| |
| |
| def _ast_get_value(node, local_vars): |
| if isinstance(node, ast.Constant): |
| return node.value |
| elif isinstance(node, ast.List): |
| return [_ast_get_value(elt, local_vars) for elt in node.elts] |
| elif isinstance(node, ast.Name): |
| if node.id in local_vars: |
| return local_vars[node.id] |
| return node.id |
| elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): |
| left = _ast_get_value(node.left, local_vars) or [] |
| right = _ast_get_value(node.right, local_vars) or [] |
| return left + right |
| return None |
| |
| |
| class _Converter: |
| |
| def __init__(self, path, old_gn_content=None): |
| self.bazel_targets = [] |
| self.test_targets = [] |
| self.public_targets = [] |
| self.packages = {} |
| self.year = str(datetime.datetime.now().year) |
| self.path = path |
| m = re.search(r'.*/absl/(.*)', path) |
| self.rel_path = m.group(1) if m else '' |
| if old_gn_content: |
| m = re.search(r'# Copyright (\d{4})', old_gn_content) |
| if m: |
| self.year = m.group(1) |
| |
| def _translate_dep(self, dep): |
| if dep.startswith('@do_not_use'): |
| return None |
| if dep == '//absl/' + self.rel_path: |
| return ':' + self.rel_path.split('/')[-1] |
| if dep.startswith(':'): |
| return dep |
| if dep.startswith('//absl/' + self.rel_path + ':'): |
| return dep[7 + len(self.rel_path):] |
| if dep.startswith('//absl'): |
| return '//third_party/abseil-cpp/' + dep[2:] |
| if dep.startswith('@googletest'): |
| return None |
| return dep |
| |
| def _translate_visibility(self, vis_list): |
| result = [] |
| for vis in vis_list: |
| if vis == '//visibility:public': |
| return [] |
| if vis == '//visibility:private': |
| result.append(':*') |
| if vis == '//absl:friends': |
| continue |
| if vis.startswith('//absl'): |
| parts = vis.split(':') |
| pkg = parts[0] |
| rule = parts[1] if len(parts) > 1 else '' |
| gn_pkg = '' if pkg[7:] == self.rel_path else ( |
| '//third_party/abseil-cpp/' + pkg[2:]) |
| if rule == '__pkg__': |
| result.append(gn_pkg + ':*') |
| elif rule == '__subpackages__': |
| result.append(gn_pkg + '/*') |
| else: |
| result.append(gn_pkg + ':' + rule) |
| if vis.startswith(':') and vis[1:] in self.packages: |
| result += self.packages[vis[1:]] |
| # In bazel targets are implicitly visible to the same BUILD file |
| # in gn such visibility should be explicit. |
| if ':*' not in result and '//third_party/abseil-cpp/absl/*' not in result: |
| result.append(':*') |
| return result |
| |
| def _translate_package(self, package): |
| if package.endswith('/...'): |
| return '//third_party/abseil-cpp' + package[1:-4] + "/*" |
| else: |
| path = package[7:] |
| gn_path = '' if path == self.rel_path else '//third_party/abseil-cpp/absl/' + path |
| return gn_path + ':*' |
| |
| # Targets that include string_view.h should depend on string_view target. |
| # For backward compatibility it is possible to depend just on 'strings', but it is |
| # better to depend on string_view directly. |
| def _need_to_add_string_view(self, bt): |
| string_target = ':strings' if self.rel_path == 'strings' else '//absl/strings' |
| if string_target not in bt.get('deps', []): |
| # If target doesn't depend on 'strings', it either doesn't use string_view, |
| # or already directly depends on string_view. Skip reading source files. |
| return False |
| |
| for s in bt.get('srcs', []) + bt.get('hdrs', []): |
| with open(os.path.join(self.path, s), 'r', encoding='utf-8') as f: |
| if re.search(r'#include "absl/strings/string_view.h"', |
| f.read()): |
| return True |
| return False |
| |
| def parse_bazel(self, content): |
| tree = ast.parse(content) |
| local_vars = { |
| 'ABSL_DEFAULT_LINKOPTS': [], |
| 'ABSL_DEFAULT_COPTS': [], |
| 'ABSL_TEST_COPTS': [] |
| } |
| default_vis = [] |
| for node in tree.body: |
| if isinstance(node, ast.Assign): |
| for target in node.targets: |
| if isinstance(target, ast.Name): |
| local_vars[target.id] = _ast_get_value( |
| node.value, local_vars) |
| if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call): |
| call = node.value |
| if isinstance(call.func, ast.Name): |
| func_name = call.func.id |
| if func_name == 'package': |
| for kw in call.keywords: |
| if kw.arg == 'default_visibility': |
| default_vis = _ast_get_value( |
| kw.value, local_vars) |
| elif func_name == 'package_group': |
| name = None |
| packages = [] |
| for kw in call.keywords: |
| if kw.arg == 'name': |
| name = _ast_get_value(kw.value, local_vars) |
| elif kw.arg == 'packages': |
| for p in _ast_get_value(kw.value, local_vars): |
| packages.append(self._translate_package(p)) |
| if name: |
| self.packages[name] = packages |
| elif func_name in ('cc_library', 'cc_test'): |
| t = {'is_test': func_name == 'cc_test'} |
| for kw in call.keywords: |
| t[kw.arg] = _ast_get_value(kw.value, local_vars) |
| if 'visibility' not in t and default_vis: |
| t['visibility'] = default_vis |
| self.bazel_targets.append(t) |
| |
| def generate(self): |
| out = [] |
| out.append(f'# Copyright {self.year} The Chromium Authors') |
| out.append( |
| '# Use of this source code is governed by a BSD-style license that' |
| ' can be') |
| out.append('# found in the LICENSE file.') |
| out.append('#') |
| out.append('# Generated file. DO NOT EDIT.') |
| out.append('') |
| out.append('import("//third_party/abseil-cpp/absl.gni")') |
| out.append('') |
| |
| for bt in self.bazel_targets: |
| name = bt.get('name') |
| if not name: |
| continue |
| |
| is_test = bt.get('is_test') |
| rule = 'absl_test' if is_test else 'absl_source_set' |
| bazel_deps = bt.get('deps', []) |
| target_name = f"{self.rel_path}:{name}" |
| |
| skip = _SKIP_TARGETS.get(target_name) |
| if skip: |
| out.append(f'# {skip}') |
| out.append(f'# {rule}("{name}")') |
| out.append(f'') |
| continue |
| |
| if '@google_benchmark//:benchmark_main' in bazel_deps: |
| # Porting benchmarks targets is not implemented. |
| continue |
| |
| if '//absl/base:exception_safety_testing' in bazel_deps: |
| out.append( |
| f'# skipped because chromium doesn\'t use c++ exceptions') |
| out.append(f'# {rule}("{name}")') |
| out.append(f'') |
| continue |
| |
| if is_test and target_name != 'base:c_header_test' and '@googletest//:gtest_main' not in bazel_deps: |
| out.append( |
| f'# {name} is excluded because defines its own main function' |
| ) |
| out.append(f'# {rule}("{name}")') |
| out.append(f'') |
| continue |
| |
| # Start writing the output. |
| out.append(_ADD_PREFIX.get(target_name, '')) |
| if target_name == 'base:c_header_test': |
| # This test is ported despite having own main function. |
| out.append('test("absl_c_header_test") {') |
| else: |
| out.append(f'{rule}("{name}") {{') |
| |
| if bt.get('testonly'): |
| out.append('testonly = true') |
| |
| srcs = bt.get('srcs', []) |
| if srcs: |
| out.append('sources = [') |
| for s in sorted(srcs): |
| out.append(f'"{s}",') |
| out.append(']') |
| |
| hdrs = bt.get('hdrs', []) + bt.get('textual_hdrs', []) |
| if hdrs: |
| out.append('public = [') |
| for h in sorted(hdrs): |
| # One exception where header is excluded |
| if target_name == 'strings:strings' and h == 'string_view.h': |
| continue |
| out.append(f'"{h}",') |
| out.append(']') |
| |
| vis = [] if is_test else self._translate_visibility( |
| bt.get('visibility', [])) |
| if target_name in _PUBLIC_TARGETS: |
| if not vis: |
| # Turn generally public target into public to abseil only. |
| vis.append("//third_party/abseil-cpp/absl/*") |
| vis.append('//third_party/abseil-cpp:absl_component_deps') |
| # empty visibility is handled by the rule template, in particular for non-tests |
| # it imply public visibility, but in component builds it is still restricted. |
| if vis: |
| out.append('visibility = [') |
| for v in sorted(vis): |
| out.append(f'"{v}",') |
| out.append(']') |
| |
| gn_deps = [] |
| for d in bazel_deps: |
| td = self._translate_dep(d) |
| if td: |
| gn_deps.append(td) |
| elif not is_test and d == '@googletest//:gtest': |
| gn_deps.append("//third_party/googletest:gmock") |
| gn_deps.append("//third_party/googletest:gtest") |
| if self._need_to_add_string_view(bt): |
| gn_deps.append( |
| self._translate_dep('//absl/strings:string_view')) |
| if gn_deps: |
| out.append('deps = [') |
| for d in sorted(gn_deps): |
| out.append(f'"{d}",') |
| out.append(']') |
| |
| out.append(_ADD_CONTENT.get(target_name, '')) |
| out.append('}') |
| out.append('') |
| |
| if target_name == 'base:c_header_test': |
| pass |
| elif is_test: |
| self.test_targets.append(target_name) |
| elif not (bt.get('testonly') or target_name in _PRIVATE_TARGETS |
| or vis): |
| self.public_targets.append(target_name) |
| |
| return '\n'.join(out) |
| |
| |
| def convert_one(path): |
| bazel_path = os.path.join(path, 'BUILD.bazel') |
| gn_path = os.path.join(path, 'BUILD.gn') |
| logging.info(f'Converting {bazel_path}') |
| old_gn = None |
| if os.path.exists(gn_path): |
| with open(gn_path, 'r', encoding='utf-8') as f: |
| old_gn = f.read() |
| |
| with open(bazel_path, 'r', encoding='utf-8') as f: |
| bazel_content = f.read() |
| |
| converter = _Converter(path, old_gn) |
| converter.parse_bazel(bazel_content) |
| |
| if not converter.bazel_targets: |
| logging.info(f'Skipping {bazel_path} (no cc_library/cc_test targets)') |
| return [], [] |
| |
| new_gn = converter.generate() |
| |
| with open(gn_path, 'w', encoding='utf-8', newline='') as f: |
| subprocess.run(['gn', 'format', '--stdin'], |
| check=True, |
| input=new_gn, |
| stdout=f, |
| stderr=subprocess.DEVNULL, |
| text=True) |
| |
| return converter.test_targets, converter.public_targets |
| |
| |
| def convert_all(root_dir): |
| all_test_targets = [] |
| all_public_targets = [] |
| for dirpath, dirnames, filenames in os.walk(os.path.join(root_dir, |
| 'absl')): |
| if 'BUILD.bazel' in filenames: |
| t, p = convert_one(dirpath) |
| all_test_targets.extend(t) |
| all_public_targets.extend(p) |
| |
| # Update root BUILD.gn |
| root_gn_path = os.path.join(root_dir, 'BUILD.gn') |
| if not os.path.exists(root_gn_path): |
| logging.error(f"Failed to find {root_gn_path}") |
| return |
| |
| logging.info(f"Updating root targets in {root_gn_path}") |
| with open(root_gn_path, 'r') as f: |
| content = f.read() |
| |
| # Update absl_component_deps |
| pattern_comp = re.compile( |
| r'(group\("absl_component_deps"\)\s*\{\s*public_deps\s*=\s*)\[([\s\S]*?)\]' |
| ) |
| libs_lines = [ |
| f'"//third_party/abseil-cpp/absl/{label}"' |
| for label in sorted(all_public_targets) |
| ] |
| new_libs_str = "[\n" + ",\n".join(libs_lines) + "\n]" |
| content = pattern_comp.sub(r'\1' + new_libs_str, content, count=1) |
| |
| # Update absl_tests |
| pattern_test = re.compile( |
| r'(test\("absl_tests"\)\s*\{[\s\S]*?deps\s*=\s*)\[([\s\S]*?)\]') |
| deps_lines = [f'"absl/{label}"' for label in sorted(all_test_targets)] |
| deps_lines.append('"//third_party/googletest:gtest_main"') |
| new_deps_str = "[" + ",\n".join(deps_lines) + "]" |
| content = pattern_test.sub(r'\1' + new_deps_str, content, count=1) |
| |
| with open(root_gn_path, 'w', encoding='utf-8', newline='') as f: |
| subprocess.run(['gn', 'format', '--stdin'], |
| check=True, |
| input=content, |
| stdout=f, |
| stderr=subprocess.DEVNULL, |
| text=True) |
| |
| |
| if __name__ == '__main__': |
| logging.getLogger().setLevel(logging.INFO) |
| |
| if len(sys.argv) == 2 and os.path.isdir(sys.argv[1]): |
| convert_one(sys.argv[1]) |
| elif len(sys.argv) < 2: |
| if not os.getcwd().endswith('src') or not os.path.exists( |
| 'chrome/browser'): |
| logging.error('Run this script from a chromium/src/ directory.') |
| exit(1) |
| convert_all(os.path.join(os.getcwd(), 'third_party', 'abseil-cpp')) |
| else: |
| logging.info('Usage: convert_bazel_to_gn.py [<dir>]') |
| exit(1) |