diff --git a/tools/cmake/bazel_to_cmake/evaluation.py b/tools/cmake/bazel_to_cmake/evaluation.py index 3a68aab52..a75f23664 100644 --- a/tools/cmake/bazel_to_cmake/evaluation.py +++ b/tools/cmake/bazel_to_cmake/evaluation.py @@ -118,7 +118,8 @@ class RuleInfo(NamedTuple): - mnemonic: Optional[str] # Only used for debugging currently. + mnemonic: str # Only used for debugging currently. + callers: List[str] outs: List[TargetId] impl: RuleImpl @@ -129,18 +130,6 @@ class Phase(enum.Enum): ANALYZE = 3 -def _get_mnemonic(currentframe) -> Optional[str]: - """Returns a mnemonic from currentframe.""" - if not currentframe: - return None - # pytype: disable=attribute-error - kind = currentframe.f_back.f_back.f_code.co_name - # pytype: enable=attribute-error - if kind.startswith("bazel_"): - kind = kind[len("bazel_") :] - return kind - - class EvaluationState: """State used while evaluating Starlark code.""" @@ -220,6 +209,8 @@ def analyze(self, targets: List[TargetId]): def add_rule( self, + _mnemonic: str, + _callers: List[str], rule_id: TargetId, impl: RuleImpl, outs: Optional[List[TargetId]] = None, @@ -239,11 +230,8 @@ def add_rule( raise ValueError(f"Duplicate rule: {rule_id.as_label()}") if outs is None: outs = [] - # kind is assigned from caller function name - currentframe = inspect.currentframe() - r = RuleInfo(mnemonic=_get_mnemonic(currentframe), outs=outs, impl=impl) - del currentframe + r = RuleInfo(_mnemonic, _callers, outs=outs, impl=impl) self._all_rules[rule_id] = r self._unanalyzed_rules.add(rule_id) self._unanalyzed_targets[rule_id] = rule_id @@ -475,7 +463,7 @@ def get_dep( for package in info[CMakePackageDepsProvider].packages: self.add_required_dep_package(package) if info.get(CMakeDepsProvider): - return info[CMakeDepsProvider].targets + return info[CMakeDepsProvider].targets.copy() # If this package is already a required dependency, return that. cmake_target = self._required_dep_targets.get(target_id) @@ -708,7 +696,12 @@ def wrapper(*args, **kwargs): class EvaluationContext(InvocationContext): """Implements InvocationContext interface for EvaluationState.""" - __slots__ = ("_state", "_caller_package_id", "_caller_package") + __slots__ = ( + "_state", + "_caller_package_id", + "_caller_package", + "_rule_location", + ) def __init__( self, @@ -720,6 +713,7 @@ def __init__( self._state = state self._caller_package_id = package_id self._caller_package = package + self._rule_location = ("", list()) assert self._caller_package_id def __repr__(self): @@ -732,6 +726,16 @@ def __repr__(self): "}\n" ) + def record_rule_location(self, mnemonic): + # Record the path of non-python callers. + s = inspect.stack() + callers = [] + for i in range(2, min(6, len(s))): + c = inspect.getframeinfo(s[i][0]) + if not c.filename.endswith(".py"): + callers.append(f"{c.filename}:{c.lineno}") + self._rule_location = (mnemonic, callers) + def update_current_package( self, package: Optional[Package] = None, @@ -829,7 +833,15 @@ def add_rule( kwargs["analyze_by_default"] = Visibility( self._caller_package ).analyze_by_default(self.resolve_target_or_label_list(visibility)) - self._state.add_rule(rule_id, impl, outs, **kwargs) + + self._state.add_rule( + self._rule_location[0], + self._rule_location[1], + rule_id, + impl, + outs, + **kwargs, + ) @trace_exception def add_analyzed_target(self, target_id: TargetId, info: TargetInfo) -> None: diff --git a/tools/cmake/bazel_to_cmake/main.py b/tools/cmake/bazel_to_cmake/main.py index ae814324b..27c598f1a 100644 --- a/tools/cmake/bazel_to_cmake/main.py +++ b/tools/cmake/bazel_to_cmake/main.py @@ -87,35 +87,7 @@ def get_bindings_from_args( return bindings -def main(): - ap = argparse.ArgumentParser() - # Used for top-level project and dependencies. - ap.add_argument("--bazel-repo-name", required=True) - ap.add_argument("--cmake-project-name", required=True) - ap.add_argument("--build-rules-output") - ap.add_argument("--cmake-binary-dir") - ap.add_argument("--include-package", action="append", default=[]) - ap.add_argument("--exclude-package", action="append", default=[]) - ap.add_argument("--repo-mapping", nargs=2, action="append", default=[]) - ap.add_argument("--extra-build", action="append", default=[]) - ap.add_argument("--exclude-target", action="append", default=[]) - ap.add_argument("--bind", action="append", default=[]) - - # Used for sub-projects only. - ap.add_argument("--load-workspace") - ap.add_argument("--target", action="append", default=[]) - - # Used for the top-level project only. - ap.add_argument("--save-workspace") - ap.add_argument("--define", action="append", default=[]) - ap.add_argument("--ignore-library", action="append", default=[]) - ap.add_argument("--cmake-vars") - ap.add_argument("--bazelrc", action="append", default=[]) - ap.add_argument("--module", action="append", default=[]) - ap.add_argument("--verbose", type=int, default=0) - - args = ap.parse_args() - +def run_main(args: argparse.Namespace): assert args.bazel_repo_name repository_id: RepositoryId = RepositoryId(args.bazel_repo_name) current_repository: CMakeRepository = CMakeRepository( @@ -327,3 +299,34 @@ def _persist_cmakepairs(target: TargetId, cmake_pair: CMakeTargetPair): pickle.dump(workspace, f) return 0 + + +def main(): + ap = argparse.ArgumentParser() + # Used for top-level project and dependencies. + ap.add_argument("--bazel-repo-name", required=True) + ap.add_argument("--cmake-project-name", required=True) + ap.add_argument("--build-rules-output") + ap.add_argument("--cmake-binary-dir") + ap.add_argument("--include-package", action="append", default=[]) + ap.add_argument("--exclude-package", action="append", default=[]) + ap.add_argument("--repo-mapping", nargs=2, action="append", default=[]) + ap.add_argument("--extra-build", action="append", default=[]) + ap.add_argument("--exclude-target", action="append", default=[]) + ap.add_argument("--bind", action="append", default=[]) + + # Used for sub-projects only. + ap.add_argument("--load-workspace") + ap.add_argument("--target", action="append", default=[]) + + # Used for the top-level project only. + ap.add_argument("--save-workspace") + ap.add_argument("--define", action="append", default=[]) + ap.add_argument("--ignore-library", action="append", default=[]) + ap.add_argument("--cmake-vars") + ap.add_argument("--bazelrc", action="append", default=[]) + ap.add_argument("--module", action="append", default=[]) + ap.add_argument("--verbose", type=int, default=0) + + args = ap.parse_args() + return run_main(args) diff --git a/tools/cmake/bazel_to_cmake/starlark/bazel_globals.py b/tools/cmake/bazel_to_cmake/starlark/bazel_globals.py index 1c9cfec0f..970461bf8 100644 --- a/tools/cmake/bazel_to_cmake/starlark/bazel_globals.py +++ b/tools/cmake/bazel_to_cmake/starlark/bazel_globals.py @@ -28,6 +28,7 @@ from .select import Select from .struct import Struct + T = TypeVar("T") @@ -158,12 +159,10 @@ def bazel_native(self): return BazelNativeBuildRules(self._context) def bazel_select(self, conditions: Dict[RelativeLabel, T]) -> Select[T]: - return Select( - { - self._context.resolve_target_or_label(condition): value - for condition, value in conditions.items() - } - ) + return Select({ + self._context.resolve_target_or_label(condition): value + for condition, value in conditions.items() + }) bazel_provider = staticmethod(provider) @@ -208,6 +207,7 @@ def register_native_build_rule(impl): name = impl.__name__ def wrapper(self, *args, **kwargs): + self._context.record_rule_location(name) # pylint: disable=protected-access return impl(self._context, *args, **kwargs) # pylint: disable=protected-access setattr(BazelNativeBuildRules, name, wrapper) @@ -219,6 +219,7 @@ def register_native_workspace_rule(impl): name = impl.__name__ def wrapper(self, *args, **kwargs): + self._context.record_rule_location(name) # pylint: disable=protected-access return impl(self._context, *args, **kwargs) # pylint: disable=protected-access setattr(BazelNativeWorkspaceRules, name, wrapper) diff --git a/tools/cmake/bazel_to_cmake/starlark/invocation_context.py b/tools/cmake/bazel_to_cmake/starlark/invocation_context.py index 6dc51b050..d26dbcea0 100644 --- a/tools/cmake/bazel_to_cmake/starlark/invocation_context.py +++ b/tools/cmake/bazel_to_cmake/starlark/invocation_context.py @@ -57,6 +57,10 @@ def caller_repository_name(self) -> str: def caller_package_id(self) -> PackageId: raise NotImplementedError("caller_package_id") + def record_rule_location(self, mnemonic): + # Adds debugging information to registered rules. + pass + def workspace_root_for_label(self, repository_id: RepositoryId) -> str: # This should return something like the Package.source_directory return self.resolve_source_root(repository_id).as_posix() diff --git a/tools/cmake/bazel_to_cmake/starlark/rule.py b/tools/cmake/bazel_to_cmake/starlark/rule.py index bcec33c4e..ded8a5805 100644 --- a/tools/cmake/bazel_to_cmake/starlark/rule.py +++ b/tools/cmake/bazel_to_cmake/starlark/rule.py @@ -111,6 +111,47 @@ def impl(ctx: RuleCtx): return Attr(handle) + @staticmethod + def string_list( + mandatory: bool = False, + allow_empty: bool = True, + *, + default: Optional[List[str]] = None, + doc: str = "", + ): + """https://bazel.build/rules/lib/attr#string_list""" + del doc + + if default is None: + default = [] + + def handle( + context: InvocationContext, + name: str, + value: Optional[List[str]], + outs: List[TargetId], + ): + if mandatory and value is None: + raise ValueError(f"Attribute {name} not specified") + if value is None: + value = default + if not value and not allow_empty: + raise ValueError(f"Attribute {name} is empty") + + del outs + del context + + def impl(ctx: RuleCtx): + setattr( + ctx.attr, + name, + ctx._context.evaluate_configurable_list(value), + ) + + return impl + + return Attr(handle) + @staticmethod def label( default: Optional[Configurable[RelativeLabel]] = None, diff --git a/tools/cmake/bazel_to_cmake/starlark/rule_test.py b/tools/cmake/bazel_to_cmake/starlark/rule_test.py index 4f2c0daac..58a1c45d1 100644 --- a/tools/cmake/bazel_to_cmake/starlark/rule_test.py +++ b/tools/cmake/bazel_to_cmake/starlark/rule_test.py @@ -35,6 +35,7 @@ def _empty_rule_impl(ctx): implementation = _empty_rule_impl, attrs = { "a_string": attr.string(default = "b"), + "a_stringlist": attr.string_list(), "a_label": attr.label(allow_files = [".header"]), "a_labellist": attr.label_list(), "a_bool": attr.bool(default = False), diff --git a/tools/cmake/bazel_to_cmake/variable_substitution.py b/tools/cmake/bazel_to_cmake/variable_substitution.py index 02de592bb..615a02661 100644 --- a/tools/cmake/bazel_to_cmake/variable_substitution.py +++ b/tools/cmake/bazel_to_cmake/variable_substitution.py @@ -78,7 +78,7 @@ def _get_relpath(path: str): rel_paths = [_get_relpath(path) for path in files_provider.paths] if not key.endswith("s"): if len(rel_paths) != 1: - raise ValueError("Expected single file but received: {rel_paths}") + raise ValueError(f"Expected single file but received: {rel_paths}") return rel_paths[0] return " ".join(rel_paths) @@ -124,45 +124,65 @@ def _get_replacement(name): # NOTE: location and make variable substitutions do not compose well since # for location substitutions to work correctly CMake generator expressions # are needed. - def _do_replacements(_cmd): - out = io.StringIO() - while True: - i = _cmd.find("$") - if i == -1: - out.write(_cmd) - return out.getvalue() - out.write(_cmd[:i]) - j = i + 1 - if _cmd[j] == "(": - # Multi character literal. - j = _cmd.find(")", i + 2) - assert j > (i + 2) - name = _cmd[i + 2 : j] - m = None - if enable_location: - m = _LOCATION_RE.fullmatch(_cmd[i + 2 : j]) - if m: - out.write( - _get_location_replacement( - _context, - cmd, - relative_to, - custom_target_deps, - m.group(1), - m.group(2), - ) - ) - else: - out.write(_get_replacement(name)) - elif _cmd[j] == "$": - # Escaped $ - out.write("$") + def _do_replace_impl(_cmd): + i = _cmd.find("$") + if i == -1: + return _cmd, None + + j = i + 1 + if _cmd[j] == "$": + return _cmd[:j], _cmd[j + 1 :] + + if _cmd[j] == "(": + closeparen = ")" + elif _cmd[j] == "{": + closeparen = "}" + else: + # Single character literal. + r = _get_replacement(_cmd[j]) + return f"{_cmd[:i]}{r}", _cmd[j + 1 :] + + # Find matching close, counting the nesting parens. + k = j + 1 + count = 1 + while k < len(_cmd): + if _cmd[k] == _cmd[j]: + count += 1 + elif _cmd[k] == closeparen: + count -= 1 + if count == 0: + break + k += 1 + + # Do replacements on the sub-string. + a, b = _do_replace_impl(_cmd[j + 1 : k]) + if b is None: + b = "" + + r = "" + if _cmd[j] == "(": + m = None + if enable_location: + m = _LOCATION_RE.fullmatch(a) + if m: + r = _get_location_replacement( + _context, + cmd, + relative_to, + custom_target_deps, + m.group(1), + m.group(2), + ) else: - # Single letter literal. - out.write(_get_replacement(_cmd[j])) - _cmd = _cmd[j + 1 :] - - return _do_replacements(cmd) + r = _get_replacement(a) + return f"{_cmd[:i]}{r}{b}", _cmd[k + 1 :] + + out = io.StringIO() + b = cmd + while b: + a, b = _do_replace_impl(b) + out.write(a) + return out.getvalue() def apply_make_variable_substitutions( diff --git a/tools/cmake/bazel_to_cmake/variable_substitution_test.py b/tools/cmake/bazel_to_cmake/variable_substitution_test.py index 8ae3b115b..66977ad91 100644 --- a/tools/cmake/bazel_to_cmake/variable_substitution_test.py +++ b/tools/cmake/bazel_to_cmake/variable_substitution_test.py @@ -33,6 +33,7 @@ from .starlark.provider import Provider from .starlark.provider import TargetInfo from .starlark.toolchain import CMAKE_TOOLCHAIN +from .variable_substitution import apply_location_and_make_variable_substitutions from .variable_substitution import apply_location_substitutions from .variable_substitution import apply_make_variable_substitutions from .variable_substitution import generate_substitutions @@ -132,6 +133,10 @@ def test_apply_make_variable_substitutions(): ctx, "$$(location $(foo)) $@", subs, [] ) + assert "ls $(dirname $x)" == apply_make_variable_substitutions( + ctx, "ls $$(dirname $$x)", subs, [] + ) + assert "${bar} x" == apply_make_variable_substitutions( ctx, "$${bar} $@", subs, [] ) @@ -168,6 +173,24 @@ def test_apply_location_substitutions(): apply_location_substitutions(ctx, "$(location a/none/target)", "", []) +def test_apply_location_and_make_variable_substitutions(): + ctx = MyContext() + + subs = {"foo": "some/file.h", "@": "x", "<": "y"} + + assert ( + "ls $(dirname bar/baz/some/file.h)" + == apply_location_and_make_variable_substitutions( + ctx, + cmd="ls $$(dirname $(location $(foo)))", + relative_to="", + custom_target_deps=[], + substitutions=subs, + toolchains=None, + ) + ) + + def test_generate_substitutions(): ctx = MyContext() a_h = (