diff --git a/rs/BUILD.bazel b/rs/BUILD.bazel index f563df71..9e376b75 100644 --- a/rs/BUILD.bazel +++ b/rs/BUILD.bazel @@ -44,6 +44,7 @@ bzl_library( srcs = ["extensions.bzl"], visibility = ["//visibility:public"], deps = [ + "//rs/platforms:triples", "//rs/private:annotations", "//rs/private:cargo_credentials", "//rs/private:cargo_workspace_graph", diff --git a/rs/extensions.bzl b/rs/extensions.bzl index 21ea4e73..e935fa5b 100644 --- a/rs/extensions.bzl +++ b/rs/extensions.bzl @@ -2,6 +2,7 @@ load("@aspect_tools_telemetry_report//:defs.bzl", "TELEMETRY") # buildifier: di load("@bazel_lib//lib:repo_utils.bzl", "repo_utils") load("@bazel_skylib//lib:paths.bzl", "paths") load("@rs_rust_host_tools//:defs.bzl", "RS_HOST_CARGO_LABEL") +load("//rs/platforms:triples.bzl", "SUPPORTED_EXEC_TRIPLES") load("//rs/private:annotations.bzl", "annotation_for", "build_annotation_map", "well_known_annotation_snippet_paths") load("//rs/private:cargo_credentials.bzl", "load_cargo_credentials") load( @@ -26,7 +27,7 @@ load("//rs/private:git_crate_metadata_repository.bzl", "git_crate_metadata_repos load("//rs/private:lint_flags.bzl", "cargo_toml_lint_flags") load("//rs/private:registry_config_repository.bzl", "registry_config_repository") load("//rs/private:registry_utils.bzl", "CRATES_IO_REGISTRY", "registry_config_repo_name") -load("//rs/private:repository_utils.bzl", "render_select") +load("//rs/private:repository_utils.bzl", "exec_target_name", "render_select") load("//rs/private:toml2json.bzl", "run_toml2json") # Bazel 8 does not define attr.label_list_dict. @@ -267,6 +268,7 @@ def _generate_hub_and_spokes( annotations = annotations, platform_triples = platform_triples, materialize_workspace_members = False, + exec_platform_triples = SUPPORTED_EXEC_TRIPLES, validate_lockfile = validate_lockfile, debug = debug, dep_label_prefix = "@%s//:" % hub_name, @@ -277,6 +279,7 @@ def _generate_hub_and_spokes( platform_cfg_attrs = workspace_resolution.platform_cfg_attrs workspace_dep_labels_by_triple = workspace_resolution.workspace_dep_labels_by_triple workspace_dep_versions_by_name = workspace_resolution.workspace_dep_versions_by_name + exec_feature_resolutions_by_fq_crate = workspace_resolution.exec_feature_resolutions_by_fq_crate _date(mctx, "set up initial deps!") @@ -290,6 +293,7 @@ def _generate_hub_and_spokes( source = package["source"] feature_resolutions = feature_resolutions_by_fq_crate[_fq_crate(crate_name, version)] + exec_feature_resolutions = exec_feature_resolutions_by_fq_crate[_fq_crate(crate_name, version)] annotation = annotation_for(annotations, crate_name, version, hub_name) suggested_annotation = None @@ -323,6 +327,7 @@ crate.annotation( gen_build_script = annotation.gen_build_script, build_script_deps = [], build_script_deps_select = _select(feature_resolutions.build_deps), + exec_build_script_deps_select = _select(exec_feature_resolutions.build_deps), build_script_data = annotation.build_script_data, build_script_data_select = annotation.build_script_data_select, build_script_env = annotation.build_script_env, @@ -338,11 +343,17 @@ crate.annotation( deps = annotation.deps, crate_tags = annotation.tags, deps_select = _select(feature_resolutions.deps), + exec_deps_select = _select(exec_feature_resolutions.deps), aliases = feature_resolutions.aliases, + exec_aliases = exec_feature_resolutions.aliases, crate_features = annotation.crate_features, crate_features_select = _select(feature_resolutions.features_enabled), + exec_crate_features_select = _select(exec_feature_resolutions.features_enabled), + target_active = bool(feature_resolutions.active), + exec_active = bool(exec_feature_resolutions.active), use_legacy_rules_rust_platforms = use_legacy_rules_rust_platforms, ) + package["exec_target_name"] = exec_target_name(struct(**kwargs), crate_name) repo_name = _spoke_repo(hub_name, crate_name, version) package["target_repo_name"] = repo_name @@ -438,6 +449,14 @@ alias( actual = "{actual}", )""".format(name = name, version = version, actual = _target_label(target_repo_name, target_package_path, name))) + exec_target = package["exec_target_name"] + if exec_target: + hub_contents.append(""" +alias( + name = "{name}-{version}_exec", + actual = "{actual}", +)""".format(name = name, version = version, actual = _target_label(target_repo_name, target_package_path, exec_target))) + for binary in annotation.gen_binaries: hub_contents.append(""" alias( @@ -927,6 +946,9 @@ _annotation = tag_class( "crate_features_select": attr.string_list_dict( doc = "A list of strings to add to a crate's `rust_library::crate_features` attribute. Keys should be the platform triplet. Value should be a list of features.", ), + "is_proc_macro": attr.bool( + doc = "Resolve this proc-macro crate and its dependencies in the exec universe.", + ), "data": attr.label_list( doc = "A list of labels to add to a crate's `rust_library::data` attribute.", ), diff --git a/rs/private/annotations.bzl b/rs/private/annotations.bzl index 2e739c0c..5428493a 100644 --- a/rs/private/annotations.bzl +++ b/rs/private/annotations.bzl @@ -16,6 +16,7 @@ def _crate_annotation( tags = [], crate_features = [], crate_features_select = {}, + is_proc_macro = False, gen_binaries = [], extra_aliased_targets = {}, rustc_flags = [], @@ -43,6 +44,7 @@ def _crate_annotation( tags = tags, crate_features = crate_features, crate_features_select = crate_features_select, + is_proc_macro = is_proc_macro, gen_binaries = gen_binaries, extra_aliased_targets = extra_aliased_targets, rustc_flags = rustc_flags, diff --git a/rs/private/cargo_workspace_graph.bzl b/rs/private/cargo_workspace_graph.bzl index 9baee0e8..0904db20 100644 --- a/rs/private/cargo_workspace_graph.bzl +++ b/rs/private/cargo_workspace_graph.bzl @@ -1,6 +1,6 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("//rs/private:cfg_parser.bzl", "cfg_matches_expr_for_cfg_attrs", "triple_to_cfg_attrs") -load("//rs/private:resolver.bzl", "resolve") +load("//rs/private:resolver.bzl", "resolve", "seed_exec_build_dependencies") load("//rs/private:select_utils.bzl", "compute_select") load("//rs/private:semver.bzl", "select_matching_version") @@ -64,6 +64,7 @@ def cfg_match_info_for_target(target, platform_cfg_attrs, cfg_match_cache): def new_feature_resolutions(package_index, possible_deps, possible_features, platform_triples): return struct( + active = set(), features_enabled = {triple: set() for triple in platform_triples}, build_deps = {triple: set() for triple in platform_triples}, deps = {triple: set() for triple in platform_triples}, @@ -420,10 +421,12 @@ def _resolve_possible_deps( packages, resolver_versions_by_name, feature_resolutions_by_fq_crate, + proc_macro_fq_crates, platform_triples, platform_cfg_attrs, cfg_match_cache, - dep_label_prefix): + dep_label_prefix, + dep_label_suffix = ""): for package in packages: name = package["name"] deps_by_name = {} @@ -460,8 +463,10 @@ def _resolve_possible_deps( dep_fq = fq_crate(dep_package, resolved_version) if dep_fq not in feature_resolutions_by_fq_crate: fail("Resolved %s dependency %s but no crate metadata was available" % (name, dep_fq)) - dep["bazel_target"] = "%s%s" % (dep_label_prefix, dep_fq) + dep["bazel_target"] = "%s%s%s" % (dep_label_prefix, dep_fq, dep_label_suffix) dep["feature_resolutions"] = feature_resolutions_by_fq_crate[dep_fq] + if dep_fq in proc_macro_fq_crates: + dep["is_proc_macro"] = True target = dep.get("target") match_info = cfg_match_info_for_target(target, platform_cfg_attrs, cfg_match_cache) @@ -472,6 +477,11 @@ def _resolve_possible_deps( else: dep["target"] = set(match_info.matches) +def _apply_annotation_features(feature_resolutions, annotation): + for triple, features in feature_resolutions.features_enabled.items(): + features.update(annotation.crate_features) + features.update(annotation.crate_features_select.get(triple, [])) + def resolve_cargo_workspace_members( ctx, *, @@ -483,6 +493,7 @@ def resolve_cargo_workspace_members( annotations, platform_triples, materialize_workspace_members, + exec_platform_triples = [], validate_lockfile = True, debug = False, dep_label_prefix = "//:", @@ -490,12 +501,12 @@ def resolve_cargo_workspace_members( watch_manifests = False, use_legacy_rules_rust_platforms = False): platform_cfg_attrs = [triple_to_cfg_attrs(triple) for triple in platform_triples] - platform_cfg_attrs_by_triple = {} - for cfg_attr in platform_cfg_attrs: - platform_cfg_attrs_by_triple[cfg_attr["_triple"]] = cfg_attr + platform_cfg_attrs_by_triple = {cfg_attr["_triple"]: cfg_attr for cfg_attr in platform_cfg_attrs} cfg_match_cache = {None: struct(matches = platform_triples, uses_feature_cfg = False)} + exec_platform_cfg_attrs_by_triple = {triple: triple_to_cfg_attrs(triple) for triple in exec_platform_triples} + workspace_member_keys = {} for package in cargo_metadata["packages"]: workspace_member_keys[(package["name"], package["version"])] = True @@ -535,16 +546,56 @@ def resolve_cargo_workspace_members( resolver_packages.append(resolver_package) + exec_feature_resolutions_by_fq_crate = {} + exec_resolver_packages = [] + if exec_platform_triples: + for package in resolver_packages: + target_resolution = package["feature_resolutions"] + target_resolution.build_deps.update({triple: set() for triple in exec_platform_triples}) + exec_resolution = new_feature_resolutions( + target_resolution.package_index, + [dict(dep) for dep in target_resolution.possible_deps], + target_resolution.possible_features, + exec_platform_triples, + ) + exec_feature_resolutions_by_fq_crate[fq_crate(package["name"], package["version"])] = exec_resolution + exec_resolver_packages.append(dict(package, feature_resolutions = exec_resolution)) + + proc_macro_fq_crates = set() + for crate, annotation_versions in annotations.items(): + for version_key, annotation in annotation_versions.items(): + if not getattr(annotation, "is_proc_macro", False): + continue + versions = resolver_versions_by_name.get(crate, []) + if version_key != "*": + versions = [version_key] if version_key in versions else [] + for version in versions: + proc_macro_fq_crates.add(fq_crate(crate, version)) + _resolve_possible_deps( resolver_packages, resolver_versions_by_name, feature_resolutions_by_fq_crate, + proc_macro_fq_crates, platform_triples, platform_cfg_attrs, cfg_match_cache, dep_label_prefix, ) + if exec_platform_triples: + _resolve_possible_deps( + exec_resolver_packages, + resolver_versions_by_name, + exec_feature_resolutions_by_fq_crate, + proc_macro_fq_crates, + exec_platform_triples, + exec_platform_cfg_attrs_by_triple.values(), + {None: struct(matches = exec_platform_triples, uses_feature_cfg = False)}, + dep_label_prefix, + dep_label_suffix = "_exec", + ) + workspace_fq_deps = compute_workspace_fq_deps(workspace_members, resolver_versions_by_name) workspace_dep_versions_by_name = {} workspace_dep_labels_by_triple = {triple: set() for triple in platform_triples} @@ -554,6 +605,7 @@ def resolve_cargo_workspace_members( ctx.watch(package["manifest_path"]) package_feature_resolutions = feature_resolutions_by_fq_crate[fq_crate(package["name"], package["version"])] + package_feature_resolutions.active.update(platform_triples) if "default" in package.get("features", {}): for triple in platform_triples: package_feature_resolutions.features_enabled[triple].add("default") @@ -611,8 +663,13 @@ def resolve_cargo_workspace_members( match_info = cfg_match_info_for_target(target, platform_cfg_attrs, cfg_match_cache) for triple in match_info.matches: + if dep.get("kind", "normal") == "build": + continue if not is_first_party_dep or materialize_workspace_members: workspace_dep_labels_by_triple[triple].add(":" + dep_name) + if dep_fq in proc_macro_fq_crates: + continue + feature_resolutions.active.add(triple) feature_resolutions.features_enabled[triple].update(features) for crate, annotation_versions in annotations.items(): @@ -625,15 +682,19 @@ def resolve_cargo_workspace_members( if not annotation.crate_features and not annotation.crate_features_select: continue for version in target_versions: - features_enabled = feature_resolutions_by_fq_crate[fq_crate(crate, version)].features_enabled - if annotation.crate_features: - for triple in platform_triples: - features_enabled[triple].update(annotation.crate_features) - for triple, features in annotation.crate_features_select.items(): - if triple in features_enabled: - features_enabled[triple].update(features) + fq = fq_crate(crate, version) + if exec_platform_triples and getattr(annotation, "is_proc_macro", False): + _apply_annotation_features(exec_feature_resolutions_by_fq_crate[fq], annotation) + else: + _apply_annotation_features(feature_resolutions_by_fq_crate[fq], annotation) + if exec_platform_triples: + _apply_annotation_features(exec_feature_resolutions_by_fq_crate[fq], annotation) + + resolve(ctx, resolver_packages, feature_resolutions_by_fq_crate, platform_cfg_attrs_by_triple, debug, include_build_dependencies = not exec_platform_triples) - resolve(ctx, resolver_packages, feature_resolutions_by_fq_crate, platform_cfg_attrs_by_triple, debug) + if exec_platform_triples: + seed_exec_build_dependencies(resolver_packages, exec_resolver_packages, exec_platform_cfg_attrs_by_triple) + resolve(ctx, exec_resolver_packages, exec_feature_resolutions_by_fq_crate, exec_platform_cfg_attrs_by_triple, debug) for package in packages: feature_resolutions = package["feature_resolutions"] @@ -651,6 +712,7 @@ def resolve_cargo_workspace_members( return struct( cfg_match_cache = cfg_match_cache, + exec_feature_resolutions_by_fq_crate = exec_feature_resolutions_by_fq_crate, feature_resolutions_by_fq_crate = feature_resolutions_by_fq_crate, platform_cfg_attrs = platform_cfg_attrs, platform_cfg_attrs_by_triple = platform_cfg_attrs_by_triple, diff --git a/rs/private/repository_utils.bzl b/rs/private/repository_utils.bzl index ae205629..9b0b7ff9 100644 --- a/rs/private/repository_utils.bzl +++ b/rs/private/repository_utils.bzl @@ -165,8 +165,8 @@ def cargo_build_file_values(rctx, cargo_toml, gen_binaries, package_path = "", g ) _RUST_CRATE_MACRO_CALL = """{indent}rust_crate( -{indent} name = {name}, -{indent} crate_name = {crate_name}, +{indent} name = {name}{name_suffix}, +{indent} crate_name = {crate_name} or {name}.replace("-", "_"), {indent} purl = {purl}, {indent} version = {version}, {indent} aliases = {{ @@ -185,7 +185,7 @@ _RUST_CRATE_MACRO_CALL = """{indent}rust_crate( {indent} edition = {edition}, {rustc_env_attr}{indent} rustc_flags = {rustc_flags}{conditional_rustc_flags}, {indent} tags = {tags}, -{indent} target_compatible_with = RESOLVED_PLATFORMS, +{indent} target_compatible_with = {target_compatible_with}, {indent} links = {links}, {indent} build_script = {build_script}, {indent} build_script_data = {build_script_data}{conditional_build_script_data}, @@ -201,22 +201,39 @@ _RUST_CRATE_MACRO_CALL = """{indent}rust_crate( {indent} has_lib = {has_lib}, {indent} binaries = {binaries}, {indent} use_legacy_rules_rust_platforms = {use_legacy_rules_rust_platforms}, +{indent} name_suffix = {crate_name_suffix}, {skip_deps_verification_attr}{indent}) """ -def render_rust_crate_call(attr, values, bazel_metadata = {}, extra_deps = "", indent = "", skip_deps_verification = False): +def _render_rust_crate_call( + attr, + values, + aliases, + crate_features_select, + deps_select, + build_deps_select, + name_suffix, + binaries, + bazel_metadata, + extra_deps, + indent, + skip_deps_verification): + target_compatible_with = "RESOLVED_PLATFORMS" + if name_suffix == "_exec" or not getattr(attr, "target_active", True) or crate_features_select.keys() != attr.crate_features_select.keys(): + target_compatible_with = "None" + # We keep conditional_crate_features unrendered here because it must be treated specially for build scripts. # See `rust_crate.bzl` for details. crate_features, conditional_crate_features = compute_select( _exclude_deps_from_features(attr.crate_features), - {platform: _exclude_deps_from_features(features) for platform, features in attr.crate_features_select.items()}, + {platform: _exclude_deps_from_features(features) for platform, features in crate_features_select.items()}, ) use_legacy_rules_rust_platforms = attr.use_legacy_rules_rust_platforms - build_deps, conditional_build_deps = render_select(attr.build_script_deps, attr.build_script_deps_select, use_legacy_rules_rust_platforms) + build_deps, conditional_build_deps = render_select(attr.build_script_deps, build_deps_select, use_legacy_rules_rust_platforms) build_script_data, conditional_build_script_data = render_select(attr.build_script_data, attr.build_script_data_select, use_legacy_rules_rust_platforms) build_script_tools, conditional_build_script_tools = render_select(attr.build_script_tools, attr.build_script_tools_select, use_legacy_rules_rust_platforms) rustc_flags, conditional_rustc_flags = render_select(attr.rustc_flags, attr.rustc_flags_select, use_legacy_rules_rust_platforms) - deps, conditional_deps = render_select(attr.deps + bazel_metadata.get("deps", []), attr.deps_select, use_legacy_rules_rust_platforms) + deps, conditional_deps = render_select(attr.deps + bazel_metadata.get("deps", []), deps_select, use_legacy_rules_rust_platforms) conditional_build_script_env = render_select_build_script_env(attr.build_script_env_select, use_legacy_rules_rust_platforms) @@ -239,17 +256,18 @@ def render_rust_crate_call(attr, values, bazel_metadata = {}, extra_deps = "", i return _RUST_CRATE_MACRO_CALL.format( indent = indent, name = values["name"], + name_suffix = " + " + repr(name_suffix) if name_suffix else "", crate_name = values["crate_name"], purl = values["purl"], version = values["version"], - aliases = list_indent.join(['"%s": "%s"' % kv for kv in attr.aliases.items()]), + aliases = list_indent.join(['"%s": "%s"' % kv for kv in aliases.items()]), deps = list_indent.join(['"%s"' % d for d in sorted(deps)]), extra_deps = extra_deps, conditional_deps = " + " + conditional_deps if conditional_deps else "", data = list_indent.join(['"%s"' % str(d) for d in attr.data]), extra_compile_data_attr = extra_compile_data_attr, crate_features = repr(sorted(crate_features)), - triples = repr(attr.crate_features_select.keys()), + triples = repr(crate_features_select.keys()), conditional_crate_features = repr(conditional_crate_features), crate_root = values["crate_root"], edition = values["edition"], @@ -257,6 +275,7 @@ def render_rust_crate_call(attr, values, bazel_metadata = {}, extra_deps = "", i rustc_flags = repr(rustc_flags), conditional_rustc_flags = " + " + conditional_rustc_flags if conditional_rustc_flags else "", tags = repr(attr.crate_tags), + target_compatible_with = target_compatible_with, links = values["links"], build_script = values["build_script"], build_script_data = repr(build_script_data), @@ -272,11 +291,103 @@ def render_rust_crate_call(attr, values, bazel_metadata = {}, extra_deps = "", i build_script_tags = repr(attr.build_script_tags), is_proc_macro = values["is_proc_macro"], has_lib = values["has_lib"], - binaries = values["binaries"], + binaries = binaries, use_legacy_rules_rust_platforms = use_legacy_rules_rust_platforms, + crate_name_suffix = repr(name_suffix), skip_deps_verification_attr = skip_deps_verification_attr, ) +def _merge_target_and_exec(target_values, exec_values): + for key, exec_value in exec_values.items(): + if key in target_values and target_values[key] != exec_value: + return None + return target_values | exec_values + +def _target_and_exec_are_mergeable(attr): + return None not in [ + _merge_target_and_exec(attr.crate_features_select, attr.exec_crate_features_select), + _merge_target_and_exec(attr.deps_select, attr.exec_deps_select), + _merge_target_and_exec(attr.build_script_deps_select, attr.exec_build_script_deps_select), + _merge_target_and_exec(attr.aliases, attr.exec_aliases), + ] + +def exec_target_name(attr, name): + if not getattr(attr, "exec_active", False): + return None + if not getattr(attr, "target_active", True) or _target_and_exec_are_mergeable(attr): + return name + return name + "_exec" + +def render_rust_crate_call(attr, values, bazel_metadata = {}, extra_deps = "", indent = "", skip_deps_verification = False): + exec_active = getattr(attr, "exec_active", False) + aliases = attr.aliases + crate_features_select = attr.crate_features_select + deps_select = attr.deps_select + build_deps_select = attr.build_script_deps_select + binaries = values["binaries"] + + if not getattr(attr, "target_active", True) and exec_active: + aliases = attr.exec_aliases + crate_features_select = attr.exec_crate_features_select + deps_select = attr.exec_deps_select + build_deps_select = attr.exec_build_script_deps_select + binaries = "{}" + elif exec_active: + merged_crate_features_select = _merge_target_and_exec(crate_features_select, attr.exec_crate_features_select) + merged_deps_select = _merge_target_and_exec(deps_select, attr.exec_deps_select) + merged_build_deps_select = _merge_target_and_exec(build_deps_select, attr.exec_build_script_deps_select) + merged_aliases = _merge_target_and_exec(aliases, attr.exec_aliases) + if None in [merged_crate_features_select, merged_deps_select, merged_build_deps_select, merged_aliases]: + target_call = _render_rust_crate_call( + attr = attr, + values = values, + aliases = aliases, + crate_features_select = crate_features_select, + deps_select = deps_select, + build_deps_select = build_deps_select, + name_suffix = "_target", + binaries = binaries, + bazel_metadata = bazel_metadata, + extra_deps = extra_deps, + indent = indent, + skip_deps_verification = skip_deps_verification, + ) + exec_call = _render_rust_crate_call( + attr = attr, + values = values, + aliases = attr.exec_aliases, + crate_features_select = attr.exec_crate_features_select, + deps_select = attr.exec_deps_select, + build_deps_select = attr.exec_build_script_deps_select, + name_suffix = "_exec", + binaries = "{}", + bazel_metadata = bazel_metadata, + extra_deps = extra_deps, + indent = indent, + skip_deps_verification = skip_deps_verification, + ) + return target_call + "\n" + exec_call + + aliases = merged_aliases + crate_features_select = merged_crate_features_select + deps_select = merged_deps_select + build_deps_select = merged_build_deps_select + + return _render_rust_crate_call( + attr = attr, + values = values, + aliases = aliases, + crate_features_select = crate_features_select, + deps_select = deps_select, + build_deps_select = build_deps_select, + name_suffix = "", + binaries = binaries, + bazel_metadata = bazel_metadata, + extra_deps = extra_deps, + indent = indent, + skip_deps_verification = skip_deps_verification, + ) + def render_build_file_content(rctx, attr, values, bazel_metadata = {}): additive_build_file_content = "" if attr.additive_build_file: @@ -299,6 +410,7 @@ rust_crate_attrs = { "gen_build_script": attr.string(), "build_script_deps": attr.label_list(), "build_script_deps_select": _label_list_dict(), + "exec_build_script_deps_select": _label_list_dict(), "build_script_data": attr.label_list(), "build_script_data_select": _label_list_dict(), "build_script_env": attr.string_dict(), @@ -314,9 +426,14 @@ rust_crate_attrs = { "data": attr.label_list(), "deps": attr.label_list(), "deps_select": _label_list_dict(), + "exec_deps_select": _label_list_dict(), "aliases": attr.string_dict(), + "exec_aliases": attr.string_dict(), "crate_features": attr.string_list(), "crate_features_select": attr.string_list_dict(), + "exec_crate_features_select": attr.string_list_dict(), + "target_active": attr.bool(), + "exec_active": attr.bool(), "use_legacy_rules_rust_platforms": attr.bool(), } diff --git a/rs/private/resolver.bzl b/rs/private/resolver.bzl index dc122de0..f36735f8 100644 --- a/rs/private/resolver.bzl +++ b/rs/private/resolver.bzl @@ -16,8 +16,7 @@ def _count(feature_resolutions_by_fq_crate): return n def _dep_target_matches_triple(dep, triple, package_feature_set, cfg_attrs_by_triple): - remaining = dep["target"] - if triple not in remaining: + if triple not in dep["target"]: return False if not dep.get("feature_sensitive", False): @@ -30,7 +29,7 @@ def _dep_target_matches_triple(dep, triple, package_feature_set, cfg_attrs_by_tr features = package_feature_set, ).matches) -def _resolve_one_round(packages, dirty_package_indices, cfg_attrs_by_triple, debug): +def _resolve_one_round(packages, dirty_package_indices, cfg_attrs_by_triple, debug, include_build_dependencies): new_dirty_package_indices = set() for index in dirty_package_indices: @@ -43,13 +42,13 @@ def _resolve_one_round(packages, dirty_package_indices, cfg_attrs_by_triple, deb deps = feature_resolutions.deps if _propagate_feature_enablement( - package_changed, new_dirty_package_indices, package, features_enabled, feature_resolutions, cfg_attrs_by_triple, debug, + include_build_dependencies, ): package_changed = True @@ -60,6 +59,10 @@ def _resolve_one_round(packages, dirty_package_indices, cfg_attrs_by_triple, deb continue kind = dep.get("kind", "normal") + if kind == "build" and not include_build_dependencies: + continue + + defer_proc_macro = dep.get("is_proc_macro", False) and not include_build_dependencies dep_feature_resolutions = dep["feature_resolutions"] @@ -77,38 +80,38 @@ def _resolve_one_round(packages, dirty_package_indices, cfg_attrs_by_triple, deb else: match = dep["target"] - to_remove = None for triple in match: + if triple not in feature_resolutions.active: + continue + if optional: features_for_triple = features_enabled[triple] if dep_name not in features_for_triple and prefixed_dep_alias not in features_for_triple: continue triple_deps = deps[triple] if kind == "normal" else feature_resolutions.build_deps[triple] - if package_changed or bazel_target not in triple_deps: + if bazel_target not in triple_deps: package_changed = True triple_deps.add(bazel_target) if has_alias: feature_resolutions.aliases[bazel_target] = dep_name.replace("-", "_") + if defer_proc_macro: + continue + triple_features = dep_feature_resolutions.features_enabled[triple] + if triple not in dep_feature_resolutions.active: + dep_feature_resolutions.active.add(triple) + new_dirty_package_indices.add(dep_feature_resolutions.package_index) + dep_features = dep.get("features") if dep_features: prev_length = len(triple_features) triple_features.update(dep_features) if prev_length != len(triple_features): new_dirty_package_indices.add(dep_feature_resolutions.package_index) - if not to_remove: - to_remove = set() - to_remove.add(triple) - - if to_remove: - if len(to_remove) == len(match): - dep["bazel_target"] = None - else: - match.difference_update(to_remove) if package_changed: new_dirty_package_indices.add(index) @@ -116,17 +119,18 @@ def _resolve_one_round(packages, dirty_package_indices, cfg_attrs_by_triple, deb return new_dirty_package_indices def _propagate_feature_enablement( - package_changed, dirty_package_indices, package, features_enabled, feature_resolutions, cfg_attrs_by_triple, - debug): + debug, + include_build_dependencies): + package_changed = False possible_features = feature_resolutions.possible_features for triple, feature_set in features_enabled.items(): - if not feature_set: + if triple not in feature_resolutions.active or not feature_set: continue # Enable any features that are implied by previously-enabled features. @@ -146,7 +150,7 @@ def _propagate_feature_enablement( dep_name = feature[:idx] dep_feature = feature[idx + 1:] - dep_optional = False + has_optional_dependency = False optional_marker = False if dep_name[-1] == "?": optional_marker = True @@ -154,19 +158,30 @@ def _propagate_feature_enablement( found = False for dep in feature_resolutions.possible_deps: - if dep_name == dep["name"] and _dep_target_matches_triple(dep, triple, feature_set, cfg_attrs_by_triple): - found = True - dep_optional = dep.get("optional", False) - if not optional_marker or not dep_optional or dep_name in feature_set or ("dep:" + dep_name) in feature_set: - dep_feature_resolutions = dep["feature_resolutions"] - triple_features = dep_feature_resolutions.features_enabled[triple] - if dep_feature not in triple_features: - triple_features.add(dep_feature) - dirty_package_indices.add(dep_feature_resolutions.package_index) - break + if dep_name != dep["name"]: + continue + + defer_dependency = not include_build_dependencies and (dep.get("kind", "normal") == "build" or dep.get("is_proc_macro", False)) + if not defer_dependency and not _dep_target_matches_triple(dep, triple, feature_set, cfg_attrs_by_triple): + continue + + found = True + dep_optional = dep.get("optional", False) + has_optional_dependency = has_optional_dependency or dep_optional + if optional_marker and dep_optional and dep_name not in feature_set and ("dep:" + dep_name) not in feature_set: + continue + + if defer_dependency: + dep.setdefault("deferred_features", set()).add(dep_feature) + else: + dep_feature_resolutions = dep["feature_resolutions"] + triple_features = dep_feature_resolutions.features_enabled[triple] + if dep_feature not in triple_features: + triple_features.add(dep_feature) + dirty_package_indices.add(dep_feature_resolutions.package_index) # Only optional deps need to be explicitly enabled when a subfeature is toggled. - if dep_optional and (not optional_marker) and dep_name not in feature_set: + if has_optional_dependency and (not optional_marker) and dep_name not in feature_set: package_changed = True feature_set.add(dep_name) @@ -177,19 +192,59 @@ def _propagate_feature_enablement( _MAX_ROUNDS = 50 -def resolve(mctx, packages, feature_resolutions_by_fq_crate, cfg_attrs_by_triple, debug): +def resolve(mctx, packages, feature_resolutions_by_fq_crate, cfg_attrs_by_triple, debug, include_build_dependencies = True): # Do some rounds of mutual resolution; bail when no more changes dirty_package_indices = range(len(packages)) for i in range(_MAX_ROUNDS): mctx.report_progress("Running round %s of dependency/feature resolution" % i) - dirty_package_indices = _resolve_one_round(packages, dirty_package_indices, cfg_attrs_by_triple, debug) + dirty_package_indices = _resolve_one_round(packages, dirty_package_indices, cfg_attrs_by_triple, debug, include_build_dependencies) if not dirty_package_indices: if debug: count = _count(feature_resolutions_by_fq_crate) print("Got count", count, "in", i + 1, "rounds") - break + return dirty_package_indices = sorted(dirty_package_indices) - if i == _MAX_ROUNDS: - fail("Resolution did not converge! This is likely a bug in rules_rs, please report it to github.com/hermeticbuild/rules_rs") + fail("Resolution did not converge! This is likely a bug in rules_rs, please report it to github.com/hermeticbuild/rules_rs") + +def seed_exec_build_dependencies(packages, exec_packages, exec_cfg_attrs_by_triple): + """Seeds exec resolution from build dependencies and annotated proc macros.""" + for package, exec_package in zip(packages, exec_packages): + target_resolution = package["feature_resolutions"] + exec_resolution = exec_package["feature_resolutions"] + + if not target_resolution.active: + continue + + target_features = set() + for triple in target_resolution.active: + target_features.update(target_resolution.features_enabled[triple]) + + for target_dep, dep in zip(target_resolution.possible_deps, exec_resolution.possible_deps): + bazel_target = dep.get("bazel_target") + is_build_dependency = dep.get("kind", "normal") == "build" + is_proc_macro = dep.get("is_proc_macro", False) + if not bazel_target or not (is_build_dependency or is_proc_macro): + continue + + dep_name = dep["name"] + if dep.get("optional", False) and dep_name not in target_features and ("dep:" + dep_name) not in target_features: + continue + + target_bazel_target = target_dep.get("bazel_target") + if is_proc_macro and not any([target_bazel_target in deps for deps in target_resolution.deps.values()]): + continue + + dep_resolution = dep["feature_resolutions"] + exec_triples = dep_resolution.features_enabled.keys() if is_proc_macro else dep["target"] + for exec_triple in exec_triples: + if is_build_dependency: + if not _dep_target_matches_triple(dep, exec_triple, target_features, exec_cfg_attrs_by_triple): + continue + target_resolution.build_deps[exec_triple].add(bazel_target) + if "package" in dep: + target_resolution.aliases[bazel_target] = dep_name.replace("-", "_") + dep_resolution.active.add(exec_triple) + dep_resolution.features_enabled[exec_triple].update(dep.get("features", [])) + dep_resolution.features_enabled[exec_triple].update(target_dep.get("deferred_features", [])) diff --git a/rs/rules_rust.bzl b/rs/rules_rust.bzl index 7caba377..b4f4596a 100644 --- a/rs/rules_rust.bzl +++ b/rs/rules_rust.bzl @@ -31,9 +31,9 @@ def _rules_rust_impl(mctx): http_archive( name = "rules_rust", - integrity = "sha256-gf19wNwRaWAqlUwFMLUfQq51YKGtSc7zU5TpXxGYfhc=", - strip_prefix = "rules_rust-0a4149d8a1af5f5881e73f7e2d5ff490b633fea4", - url = "https://github.com/hermeticbuild/rules_rust/archive/0a4149d8a1af5f5881e73f7e2d5ff490b633fea4.tar.gz", + integrity = "sha256-MyZ49sySXriUUCZjcHpHHvMtHBVs35IfSfBIOzp5i1I=", + strip_prefix = "rules_rust-7837073014b566291368027aa116cd03fe108049", + url = "https://github.com/hermeticbuild/rules_rust/archive/7837073014b566291368027aa116cd03fe108049.tar.gz", patches = patches, patch_strip = strip, ) diff --git a/rs/rust_crate.bzl b/rs/rust_crate.bzl index 8af42ef0..d9e39a3e 100644 --- a/rs/rust_crate.bzl +++ b/rs/rust_crate.bzl @@ -45,13 +45,33 @@ def rust_crate( use_legacy_rules_rust_platforms, extra_compile_data = [], rustc_env = {}, - skip_deps_verification = False): + skip_deps_verification = False, + name_suffix = ""): + build_script_name = "_bs" + name_suffix + if target_compatible_with == None: + target_compatible_with = select({ + _platform(triple, use_legacy_rules_rust_platforms): [] + for triple in triples + } | {"//conditions:default": ["@platforms//:incompatible"]}) + package_metadata( name = name + "_package_metadata", purl = purl, visibility = ["//visibility:public"], ) + if name_suffix == "_exec": + target_exec_alias_name = name.removesuffix(name_suffix) + native.alias( + name = target_exec_alias_name, + actual = select({ + "@rules_rust//cargo/settings:use_exec_features_enabled": name, + "//conditions:default": target_exec_alias_name + "_target", + }), + tags = ["crate-name=" + (crate_name or target_exec_alias_name)] + tags, + visibility = ["//visibility:public"], + ) + compile_data = native.glob( include = ["**"], exclude = [ @@ -74,7 +94,7 @@ def rust_crate( ) default_tags = [ - "crate-name=" + name, + "crate-name=" + (crate_name or name), "manual", "noclippy", "norustfmt", @@ -114,29 +134,29 @@ def rust_crate( # The build script is cfg-exec, but the features must be selected according to the target. # Only stamp out one target per triple when there are per-platform feature deltas. for triple in triples: - build_script_name = "_bs_" + triple - branches[_platform(triple, use_legacy_rules_rust_platforms)] = build_script_name + triple_build_script_name = build_script_name + "_" + triple + branches[_platform(triple, use_legacy_rules_rust_platforms)] = triple_build_script_name cargo_build_script( - name = build_script_name, + name = triple_build_script_name, crate_features = crate_features + conditional_crate_features.get(triple, []), **build_script_kwargs ) native.alias( - name = "_bs", + name = build_script_name, actual = select(branches), tags = build_script_target_tags, ) else: cargo_build_script( - name = "_bs", + name = build_script_name, crate_features = crate_features, **build_script_kwargs ) - maybe_build_script = ["_bs"] + maybe_build_script = [build_script_name] else: maybe_build_script = [] diff --git a/test/MODULE.bazel b/test/MODULE.bazel index 37fcd9ec..e790997e 100644 --- a/test/MODULE.bazel +++ b/test/MODULE.bazel @@ -152,6 +152,7 @@ TESTS = [ "build_script_env_select", "build_script_runtime_env", "build_dep_features", + "build_exec_feature_split", "cfg_feature_target_dep", "empty_workspace", "feature_name_overrides_implicit_dep", @@ -202,6 +203,12 @@ TESTS = [ [use_repo(crate, test) for test in TESTS] +crate.annotation( + crate = "feature-split-proc-macro", + is_proc_macro = True, + repositories = ["build_exec_feature_split"], +) + # Workspace member whose Cargo.toml lives at the bazel workspace root. crate.from_cargo( name = "root_package", diff --git a/test/MODULE.bazel.lock b/test/MODULE.bazel.lock index 4c39a38a..8e1fa0c7 100644 --- a/test/MODULE.bazel.lock +++ b/test/MODULE.bazel.lock @@ -2435,6 +2435,10 @@ "paste_1.0.15": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"paste-test-suite\",\"req\":\"^0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.49\"}],\"features\":{}}", "pastey_0.1.1": "{\"dependencies\":[],\"features\":{}}", "pastey_0.2.3": "{\"dependencies\":[],\"features\":{}}", + "path+build_exec_feature_split/vendor/consumer_feature-split-consumer": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"feature-split-proc-macro\",\"optional\":true},{\"default_features\":true,\"features\":[\"target_feature\"],\"name\":\"feature-split-shared\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"feature-split-unannotated-proc-macro\",\"optional\":false},{\"default_features\":true,\"features\":[\"exec_feature\"],\"kind\":\"build\",\"name\":\"feature-split-shared\",\"optional\":false}],\"features\":{\"default\":[\"macros\"],\"macros\":[\"dep:feature-split-proc-macro\",\"feature-split-proc-macro?/exec_feature\"]},\"strip_prefix\":\"\"}", + "path+build_exec_feature_split/vendor/proc-macro_feature-split-proc-macro": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"feature-split-shared\",\"optional\":false}],\"features\":{\"exec_feature\":[\"feature-split-shared/exec_feature\"]},\"strip_prefix\":\"\"}", + "path+build_exec_feature_split/vendor/shared_feature-split-shared": "{\"dependencies\":[],\"features\":{\"exec_feature\":[],\"target_feature\":[],\"unannotated_proc_macro_feature\":[]},\"strip_prefix\":\"\"}", + "path+build_exec_feature_split/vendor/unannotated-proc-macro_feature-split-unannotated-proc-macro": "{\"dependencies\":[{\"default_features\":true,\"features\":[\"unannotated_proc_macro_feature\"],\"name\":\"feature-split-shared\",\"optional\":false}],\"features\":{},\"strip_prefix\":\"\"}", "path+cfg_feature_target_dep/inner_inner": "{\"dependencies\":[{\"name\":\"itoa\",\"req\":\"1.0.15\",\"target\":\"cfg(feature = \\\"with_itoa\\\")\"}],\"features\":{\"with_itoa\":[]},\"strip_prefix\":\"\"}", "path+first_party_feature_propagation/../first_party_feature_propagation.deps/dep_leaf_dep_leaf": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"itoa\",\"optional\":true,\"req\":\"1.0.15\"},{\"default_features\":true,\"features\":[],\"name\":\"ryu\",\"optional\":true,\"req\":\"1.0.20\"}],\"features\":{\"default\":[],\"default_extra\":[\"dep:itoa\"],\"with_extra\":[\"dep:ryu\"]},\"strip_prefix\":\"\"}", "path+vendored_crate_override/vendor/itoa_itoa": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"no-panic\",\"optional\":true,\"req\":\"0.1\"}],\"features\":{},\"strip_prefix\":\"\"}", diff --git a/test/build_exec_feature_split/Cargo.lock b/test/build_exec_feature_split/Cargo.lock new file mode 100644 index 00000000..82c93d4c --- /dev/null +++ b/test/build_exec_feature_split/Cargo.lock @@ -0,0 +1,37 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "build-exec-feature-split" +version = "0.1.0" +dependencies = [ + "feature-split-consumer", +] + +[[package]] +name = "feature-split-consumer" +version = "0.1.0" +dependencies = [ + "feature-split-proc-macro", + "feature-split-shared", + "feature-split-unannotated-proc-macro", +] + +[[package]] +name = "feature-split-proc-macro" +version = "0.1.0" +dependencies = [ + "feature-split-shared", +] + +[[package]] +name = "feature-split-shared" +version = "0.1.0" + +[[package]] +name = "feature-split-unannotated-proc-macro" +version = "0.1.0" +dependencies = [ + "feature-split-shared", +] diff --git a/test/build_exec_feature_split/Cargo.toml b/test/build_exec_feature_split/Cargo.toml new file mode 100644 index 00000000..c473ecba --- /dev/null +++ b/test/build_exec_feature_split/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "build-exec-feature-split" +version = "0.1.0" + +[workspace] +resolver = "2" +exclude = ["vendor/consumer", "vendor/proc-macro", "vendor/shared", "vendor/unannotated-proc-macro"] + +[dependencies] +feature-split-consumer = { path = "vendor/consumer" } + +[patch.crates-io] +feature-split-proc-macro = { path = "vendor/proc-macro" } +feature-split-shared = { path = "vendor/shared" } +feature-split-unannotated-proc-macro = { path = "vendor/unannotated-proc-macro" } diff --git a/test/build_exec_feature_split/src/lib.rs b/test/build_exec_feature_split/src/lib.rs new file mode 100644 index 00000000..e69de29b diff --git a/test/build_exec_feature_split/vendor/consumer/Cargo.toml b/test/build_exec_feature_split/vendor/consumer/Cargo.toml new file mode 100644 index 00000000..c8cf2d6f --- /dev/null +++ b/test/build_exec_feature_split/vendor/consumer/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "feature-split-consumer" +version = "0.1.0" +edition = "2024" + +[features] +default = ["macros"] +macros = ["dep:feature-split-proc-macro", "feature-split-proc-macro?/exec_feature"] + +[dependencies] +# The target crate must not receive exec_feature from build-dependencies. +feature-split-shared = { path = "../shared", features = ["target_feature"] } +feature-split-proc-macro = { path = "../proc-macro", optional = true } +feature-split-unannotated-proc-macro = { path = "../unannotated-proc-macro" } + +[build-dependencies] +# The build script must not receive target_feature from dependencies. +feature-split-shared = { path = "../shared", features = ["exec_feature"] } diff --git a/test/build_exec_feature_split/vendor/consumer/build.rs b/test/build_exec_feature_split/vendor/consumer/build.rs new file mode 100644 index 00000000..6a8e211f --- /dev/null +++ b/test/build_exec_feature_split/vendor/consumer/build.rs @@ -0,0 +1,3 @@ +fn main() { + feature_split_shared::exec_only(); +} diff --git a/test/build_exec_feature_split/vendor/consumer/src/lib.rs b/test/build_exec_feature_split/vendor/consumer/src/lib.rs new file mode 100644 index 00000000..4f2bd5ae --- /dev/null +++ b/test/build_exec_feature_split/vendor/consumer/src/lib.rs @@ -0,0 +1,7 @@ +#[feature_split_proc_macro::identity] +pub fn annotated_proc_macro() {} + +#[feature_split_unannotated_proc_macro::identity] +pub fn unannotated_proc_macro() {} + +pub use feature_split_shared::target_only; diff --git a/test/build_exec_feature_split/vendor/proc-macro/Cargo.toml b/test/build_exec_feature_split/vendor/proc-macro/Cargo.toml new file mode 100644 index 00000000..463d0e8a --- /dev/null +++ b/test/build_exec_feature_split/vendor/proc-macro/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "feature-split-proc-macro" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[features] +exec_feature = ["feature-split-shared/exec_feature"] + +[dependencies] +feature-split-shared = { path = "../shared" } diff --git a/test/build_exec_feature_split/vendor/proc-macro/src/lib.rs b/test/build_exec_feature_split/vendor/proc-macro/src/lib.rs new file mode 100644 index 00000000..1b5784fb --- /dev/null +++ b/test/build_exec_feature_split/vendor/proc-macro/src/lib.rs @@ -0,0 +1,7 @@ +use proc_macro::TokenStream; + +#[proc_macro_attribute] +pub fn identity(_attr: TokenStream, item: TokenStream) -> TokenStream { + feature_split_shared::exec_only(); + item +} diff --git a/test/build_exec_feature_split/vendor/shared/Cargo.toml b/test/build_exec_feature_split/vendor/shared/Cargo.toml new file mode 100644 index 00000000..81915506 --- /dev/null +++ b/test/build_exec_feature_split/vendor/shared/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "feature-split-shared" +version = "0.1.0" +edition = "2024" + +[features] +exec_feature = [] +target_feature = [] +unannotated_proc_macro_feature = [] diff --git a/test/build_exec_feature_split/vendor/shared/src/lib.rs b/test/build_exec_feature_split/vendor/shared/src/lib.rs new file mode 100644 index 00000000..58a9f6e8 --- /dev/null +++ b/test/build_exec_feature_split/vendor/shared/src/lib.rs @@ -0,0 +1,14 @@ +#[cfg(all(feature = "exec_feature", feature = "target_feature"))] +compile_error!("target and exec features were unified"); + +#[cfg(all(feature = "exec_feature", feature = "unannotated_proc_macro_feature"))] +compile_error!("unannotated proc-macro features entered exec resolution"); + +#[cfg(feature = "exec_feature")] +pub fn exec_only() {} + +#[cfg(feature = "target_feature")] +pub fn target_only() {} + +#[cfg(feature = "unannotated_proc_macro_feature")] +pub fn unannotated_proc_macro_only() {} diff --git a/test/build_exec_feature_split/vendor/unannotated-proc-macro/Cargo.toml b/test/build_exec_feature_split/vendor/unannotated-proc-macro/Cargo.toml new file mode 100644 index 00000000..7d249b60 --- /dev/null +++ b/test/build_exec_feature_split/vendor/unannotated-proc-macro/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "feature-split-unannotated-proc-macro" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +feature-split-shared = { path = "../shared", features = ["unannotated_proc_macro_feature"] } diff --git a/test/build_exec_feature_split/vendor/unannotated-proc-macro/src/lib.rs b/test/build_exec_feature_split/vendor/unannotated-proc-macro/src/lib.rs new file mode 100644 index 00000000..3c2bc3ea --- /dev/null +++ b/test/build_exec_feature_split/vendor/unannotated-proc-macro/src/lib.rs @@ -0,0 +1,7 @@ +use proc_macro::TokenStream; + +#[proc_macro_attribute] +pub fn identity(_attr: TokenStream, item: TokenStream) -> TokenStream { + feature_split_shared::unannotated_proc_macro_only(); + item +}