diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6fbbaa6759..92c660ecd0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,7 @@ Rules used to compile Rust code. * `rust_binary` * `rust_test` * `rust_static_library` +* `rust_dylib_library` * `rust_shared_library` * `rust_proc_macro` * `cargo_build_script` diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index d1339df731..26e906f3b2 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -96,6 +96,22 @@ stardoc( deps = [":all_docs"], ) +stardoc( + name = "rust_cdylib_library", + out = "src/rust_cdylib_library.md", + input = "@rules_rust//rust:rust_cdylib_library.bzl", + symbol_names = ["rust_cdylib_library"], + deps = [":all_docs"], +) + +stardoc( + name = "rust_dylib_library", + out = "src/rust_dylib_library.md", + input = "@rules_rust//rust:rust_dylib_library.bzl", + symbol_names = ["rust_dylib_library"], + deps = [":all_docs"], +) + stardoc( name = "rust_shared_library", out = "src/rust_shared_library.md", @@ -345,12 +361,14 @@ mdbook( ":rust_analyzer_toolchain", ":rust_binary", ":rust_bindgen", + ":rust_cdylib_library", ":rust_bzlmod", ":rust_clippy", ":rust_clippy_aspect", ":rust_clippy_test", ":rust_doc", ":rust_doc_test", + ":rust_dylib_library", ":rust_library", ":rust_library_group", ":rust_lint_config", diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index ed984eaee0..4224c1031c 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -10,6 +10,8 @@ - [rust_library](./rust_library.md) - [rust_library_group](./rust_library_group.md) - [rust_lint_config](./rust_lint_config.md) + - [rust_cdylib_library](./rust_cdylib_library.md) + - [rust_dylib_library](./rust_dylib_library.md) - [rust_proc_macro](./rust_proc_macro.md) - [rust_shared_library](./rust_shared_library.md) - [rust_static_library](./rust_static_library.md) diff --git a/docs/src/rust.md b/docs/src/rust.md index 0ca031a7da..99927458c9 100644 --- a/docs/src/rust.md +++ b/docs/src/rust.md @@ -18,9 +18,13 @@ directly to the crate types Cargo produces. other Rust targets can depend on. - [rust_static_library](./rust_static_library.md) — Build a `staticlib` (`--crate-type=staticlib`) for linking Rust code into a C/C++ binary. -- [rust_shared_library](./rust_shared_library.md) — Build a `cdylib` +- [rust_cdylib_library](./rust_cdylib_library.md) — Build a `cdylib` (`--crate-type=cdylib`) for use as a shared library from C/C++ or other languages. +- [rust_dylib_library](./rust_dylib_library.md) — Build a `dylib` + (`--crate-type=dylib`) for use as a shared library with the unstable Rust ABI. +- [rust_shared_library](./rust_shared_library.md) — Convenience alias over + `rust_cdylib_library`. - [rust_proc_macro](./rust_proc_macro.md) — Build a procedural macro crate (`--crate-type=proc-macro`) that other Rust targets can consume as a compile-time plugin. diff --git a/rust/defs.bzl b/rust/defs.bzl index 426c743846..0e4b8e7334 100644 --- a/rust/defs.bzl +++ b/rust/defs.bzl @@ -74,10 +74,11 @@ load("//rust/private:common.bzl", _rust_common = "rust_common") load( "//rust/private:rust.bzl", _rust_binary = "rust_binary", + _rust_cdylib_library = "rust_cdylib_library", + _rust_dylib_library = "rust_dylib_library", _rust_library = "rust_library", _rust_library_group = "rust_library_group", _rust_proc_macro = "rust_proc_macro", - _rust_shared_library = "rust_shared_library", _rust_static_library = "rust_static_library", _rust_test = "rust_test", ) @@ -103,7 +104,10 @@ rust_library = _rust_library rust_static_library = _rust_static_library # See @rules_rust//rust/private:rust.bzl for a complete description. -rust_shared_library = _rust_shared_library +rust_dylib_library = _rust_dylib_library +# See @rules_rust//rust/private:rust.bzl for a complete description. + +rust_shared_library = _rust_cdylib_library # See @rules_rust//rust/private:rust.bzl for a complete description. rust_proc_macro = _rust_proc_macro diff --git a/rust/private/providers.bzl b/rust/private/providers.bzl index f3e8b682ec..fdb952c155 100644 --- a/rust/private/providers.bzl +++ b/rust/private/providers.bzl @@ -221,6 +221,8 @@ AllocatorLibrariesInfo = provider( "global_allocator_library": "Optional[CcInfo]: used when a global rust allocator is used", "libstd_and_allocator_ccinfo": "Optional[CcInfo]: used when the default rust allocator is used", "libstd_and_global_allocator_ccinfo": "Optional[CcInfo]: used when a global rust allocator is used", + "libstd_dylib_and_allocator_ccinfo": "Optional[CcInfo]: used when the default rust allocator is used with a dylib stdlib", + "libstd_dylib_and_global_allocator_ccinfo": "Optional[CcInfo]: used when a global rust allocator is used with a dylib stdlib", "nostd_and_global_allocator_ccinfo": "Optional[CcInfo]: used when nostd with a global rust allocator is used", }, ) diff --git a/rust/private/rust.bzl b/rust/private/rust.bzl index 66293bf766..cdc1d9646d 100644 --- a/rust/private/rust.bzl +++ b/rust/private/rust.bzl @@ -159,8 +159,29 @@ def _rust_static_library_impl(ctx): """ return _rust_library_common(ctx, "staticlib") -def _rust_shared_library_impl(ctx): - """The implementation of the `rust_shared_library` rule. +def _rust_dylib_library_impl(ctx): + """The implementation of the `rust_dylib_library` rule. + + This rule provides CcInfo, so it can be used everywhere Bazel expects + rules_cc. + + **Note**: When dynamic libraries are listed as dependencies for other Rust + binaries they can induce errors from multiply defined symbols, causing + linker errors in rustc. Some libraries in the dependency graph may need to + be converted to dynamic libraries, and/or have the standard library + dynamically linked (`link_std_dylib`) to avoid this. These rules do not + attempt to resolve these linking issues automatically. + + Args: + ctx (ctx): The rule's context object + + Returns: + list: A list of providers. + """ + return _rust_library_common(ctx, "dylib") + +def _rust_cdylib_library_impl(ctx): + """The implementation of the `rust_cdylib_library` rule. This rule provides CcInfo, so it can be used everywhere Bazel expects rules_cc. @@ -987,6 +1008,15 @@ _RUST_TEST_ATTRS = { "env_inherit": attr.string_list( doc = "Specifies additional environment variables to inherit from the external environment when the test is executed by bazel test.", ), + "link_std_dylib": attr.bool( + mandatory = False, + default = False, + doc = dedent("""\ + Flag to dynamically link the standard library as a Rust dylib .so object when building this test. + + Default is false. This is often required when testing a target that depends on a Rust ABI dylib. + """), + ), "use_libtest_harness": attr.bool( mandatory = False, default = True, @@ -1082,6 +1112,53 @@ rust_library = rule( """), ) +rust_dylib_library = rule( + implementation = _rust_dylib_library_impl, + provides = COMMON_PROVIDERS, + attrs = _COMMON_ATTRS | { + "disable_pipelining": attr.bool( + default = False, + doc = dedent("""\ + Disables pipelining for this rule if it is globally enabled. + This will cause this rule to not produce a `.rmeta` file and all the dependent + crates will instead use the `.rlib` file. + """), + ), + "link_std_dylib": attr.bool( + mandatory = False, + default = True, + doc = dedent("""\ + Flag to dynamically link the standard library as a Rust dylib .so object when building this library. + + Default is true. This is typically required for Rust ABI dylibs so that the stdlib is shared + with the binary that loads them, avoiding duplicate symbols. + """), + ), + }, + fragments = ["cpp"], + toolchains = [ + str(Label("//rust:toolchain_type")), + config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), + ], + doc = dedent("""\ + Builds a shared library using the unstable Rust ABI. + + This library can be depended on by other Rust targets via --extern, + making it suitable for splitting a Rust project into separately compiled + dynamic libraries. Note that the Rust ABI is not stable across compiler + versions. + + This rule provides CcInfo, so it can be used everywhere Bazel expects `rules_cc`. + + **Note**: When dynamic libraries are listed as dependencies for other Rust + binaries they can induce errors from multiply defined symbols, causing + linker errors in rustc. Some libraries in the dependency graph may need to + be converted to dynamic libraries, and/or have the standard library + dynamically linked (`link_std_dylib`) to avoid this. These rules do not + attempt to resolve these linking issues automatically. + """), +) + def _resolve_platform(settings, attr): """Resolve the platform label for a transition, adding @ prefix if needed. @@ -1158,8 +1235,8 @@ _rust_shared_library_transition = transition( ], ) -rust_shared_library = rule( - implementation = _rust_shared_library_impl, +rust_cdylib_library = rule( + implementation = _rust_cdylib_library_impl, attrs = _COMMON_ATTRS | _PLATFORM_ATTRS | _EXPERIMENTAL_USE_CC_COMMON_LINK_ATTRS, fragments = ["cpp"], cfg = _rust_shared_library_transition, @@ -1172,7 +1249,7 @@ rust_shared_library = rule( rust_common.test_crate_info, ], doc = dedent("""\ - Builds a Rust shared library. + Builds a C ABI Rust shared library. This shared library will contain all transitively reachable crates and native objects. It is meant to be used when producing an artifact that is then consumed by some other build system @@ -1226,6 +1303,15 @@ _RUST_BINARY_ATTRS = { more complicated debugger attachment. """), ), + "link_std_dylib": attr.bool( + mandatory = False, + default = False, + doc = dedent("""\ + Flag to dynamically link the standard library as a Rust dylib .so object when building this binary. + + Default is false. This is often required when building a binary that depends on a Rust ABI dylib. + """), + ), "linker_script": attr.label( doc = dedent("""\ Link script to forward into linker via rustc options. diff --git a/rust/private/rust_allocator_libraries.bzl b/rust/private/rust_allocator_libraries.bzl index 7e2571b8bc..e1f299cd0f 100644 --- a/rust/private/rust_allocator_libraries.bzl +++ b/rust/private/rust_allocator_libraries.bzl @@ -43,7 +43,7 @@ def make_libstd_and_allocator_ccinfo( feature_configuration, label, actions, - experimental_link_std_dylib, + link_std_dylib, rust_std, allocator_library, std = "std"): @@ -54,7 +54,7 @@ def make_libstd_and_allocator_ccinfo( feature_configuration (feature_configuration): feature_configuration to be queried. label (Label): The rule's label. actions: The rule's ctx.actions object. - experimental_link_std_dylib (boolean): The value of the standard library's `_experimental_link_std_dylib(ctx)`. + link_std_dylib (boolean): If the standard library should be included as a dylib. rust_std: The Rust standard library. allocator_library (struct): The target to use for providing allocator functions. This should be a struct with either: @@ -82,6 +82,9 @@ def make_libstd_and_allocator_ccinfo( """).format(label, rust_std)) rust_stdlib_info = rust_std[rust_common.stdlib_info] + if link_std_dylib and (not rust_stdlib_info.std_dylib or not cc_toolchain): + return None + if rust_stdlib_info.self_contained_files: compilation_outputs = cc_common.create_compilation_outputs( objects = depset(rust_stdlib_info.self_contained_files), @@ -183,7 +186,7 @@ def make_libstd_and_allocator_ccinfo( order = "topological", ) - if experimental_link_std_dylib: + if link_std_dylib: # std dylib has everything so that we do not need to include all std_files std_inputs = depset( [cc_common.create_library_to_link( @@ -264,20 +267,23 @@ def _rust_allocator_libraries_impl(ctx): toolchain = find_toolchain(ctx) - def make_cc_info(info, std): + def make_cc_info(info, std, link_std_dylib): return toolchain.make_libstd_and_allocator_ccinfo( ctx.label, ctx.actions, struct(allocator_libraries_impl_info = info), std, + link_std_dylib, ) providers = [AllocatorLibrariesInfo( allocator_library = allocator_library, global_allocator_library = global_allocator_library, - libstd_and_allocator_ccinfo = make_cc_info(allocator_library, "std"), - libstd_and_global_allocator_ccinfo = make_cc_info(global_allocator_library, "std"), - nostd_and_global_allocator_ccinfo = make_cc_info(global_allocator_library, "no_std_with_alloc"), + libstd_and_allocator_ccinfo = make_cc_info(allocator_library, "std", False), + libstd_and_global_allocator_ccinfo = make_cc_info(global_allocator_library, "std", False), + nostd_and_global_allocator_ccinfo = make_cc_info(global_allocator_library, "no_std_with_alloc", False), + libstd_dylib_and_allocator_ccinfo = make_cc_info(allocator_library, "std", True), + libstd_dylib_and_global_allocator_ccinfo = make_cc_info(global_allocator_library, "std", True), )] return providers diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index d9f72bacaa..0f01c709a7 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -20,7 +20,6 @@ load( "@bazel_tools//tools/build_defs/cc:action_names.bzl", "CPP_LINK_DYNAMIC_LIBRARY_ACTION_NAME", "CPP_LINK_EXECUTABLE_ACTION_NAME", - "CPP_LINK_NODEPS_DYNAMIC_LIBRARY_ACTION_NAME", "CPP_LINK_STATIC_LIBRARY_ACTION_NAME", ) load("@rules_cc//cc/common:cc_common.bzl", "cc_common") @@ -290,6 +289,11 @@ def collect_deps( transitive_link_search_paths.append(dep_info.link_search_path_files) transitive_build_infos.append(dep_info.transitive_build_infos) + + # If the dep is a dylib, include its own CcInfo in transitive_noncrates + # so downstream binaries get the RPATH and runfiles for the .so + if crate_info.type == "dylib" and cc_info: + transitive_noncrates.append(cc_info.linking_context.linker_inputs) elif cc_info or dep_build_info: if cc_info: # This dependency is a cc_library @@ -402,7 +406,7 @@ def get_linker_and_args(ctx, crate_type, toolchain, cc_toolchain, feature_config action_name = CPP_LINK_EXECUTABLE_ACTION_NAME elif crate_type in ("dylib"): is_linking_dynamic_library = True - action_name = CPP_LINK_NODEPS_DYNAMIC_LIBRARY_ACTION_NAME + action_name = CPP_LINK_DYNAMIC_LIBRARY_ACTION_NAME elif crate_type in ("staticlib"): is_linking_dynamic_library = False action_name = CPP_LINK_STATIC_LIBRARY_ACTION_NAME @@ -442,6 +446,11 @@ def get_linker_and_args(ctx, crate_type, toolchain, cc_toolchain, feature_config ld = toolchain.linker.path ld_is_direct_driver = toolchain.linker_type == "direct" + # Make sure we include RPATHs for Rust ABI dylibs even when no cc_toolchain. + if not cc_toolchain and rpaths: + for rpath in rpaths.to_list(): + link_args.append("-Wl,-rpath,$ORIGIN/" + rpath) + # When using rust-lld directly, we still need library search paths from cc_toolchain # to find system libraries that rustc's stdlib depends on (like -lgcc_s, -lutil, etc.) # Filter link_args to only include flags that help locate libraries. @@ -956,7 +965,8 @@ def construct_arguments( skip_expanding_rustc_env = False, require_explicit_unstable_features = False, error_format = None, - allowed_unstable_rust_features = None): + allowed_unstable_rust_features = None, + link_std_dylib = False): """Builds an Args object containing common rustc flags Args: @@ -1028,6 +1038,7 @@ def construct_arguments( require_explicit_unstable_features (bool): Whether to require all unstable features to be explicitly opted in to using `-Zallow-features=...`. error_format (str, optional): Error format to pass to the `--error-format` command line argument. If set to None, uses the "_error_format" entry in `attr`. allowed_unstable_rust_features (list, optional): List of unstable Rust language features allowed for this target. + link_std_dylib (bool): Whether to dynamically link the Rust standard library using `--prefer-dynamic`. Returns: tuple: A tuple of the following items @@ -1305,7 +1316,7 @@ def construct_arguments( compilation_mode = compilation_mode, toolchain = toolchain, ) - rpaths = _compute_rpaths(toolchain, output_dir, dep_info, use_pic) + rpaths = _compute_rpaths(toolchain, output_dir, dep_info, use_pic, link_std_dylib, crate_info.output, ctx.workspace_name) else: rpaths = depset() @@ -1349,6 +1360,11 @@ def construct_arguments( include_link_flags = include_link_flags, ) + # On macOS, set the dylib install name to @rpath/ so that + # consumers find it via RPATH rather than the exec-root build path. + if crate_info.type == "dylib" and toolchain.target_os in ["macos", "darwin"]: + rustc_flags.add("--codegen=link-arg=-Wl,-install_name,@rpath/" + crate_info.output.basename) + use_metadata = _depend_on_metadata(crate_info, force_depend_on_objects) # These always need to be added, even if not linking this crate. @@ -1392,7 +1408,7 @@ def construct_arguments( map_each = _args_map_bin_dir, ) - if toolchain._experimental_link_std_dylib: + if link_std_dylib: rustc_flags.add("--codegen=prefer-dynamic") # Make bin crate data deps available to tests. @@ -1805,6 +1821,11 @@ def rustc_compile_action( dwo_outputs = ctx.actions.declare_directory(fission_directory, sibling = crate_info.output) rust_flags.append(("-Zsplit-dwarf-out-dir=%s", dwo_outputs)) + if hasattr(ctx.attr, "link_std_dylib"): + link_std_dylib = toolchain._link_std_dylib or ctx.attr.link_std_dylib + else: + link_std_dylib = toolchain._link_std_dylib + args, env_from_args = construct_arguments( ctx = ctx, attr = attr, @@ -1829,6 +1850,7 @@ def rustc_compile_action( skip_expanding_rustc_env = skip_expanding_rustc_env, require_explicit_unstable_features = require_explicit_unstable_features, allowed_unstable_rust_features = allowed_unstable_rust_features, + link_std_dylib = link_std_dylib, ) args_metadata = None @@ -1857,6 +1879,7 @@ def rustc_compile_action( build_metadata = True, require_explicit_unstable_features = require_explicit_unstable_features, allowed_unstable_rust_features = allowed_unstable_rust_features, + link_std_dylib = link_std_dylib, ) env = dict(ctx.configuration.default_shell_env) @@ -1984,7 +2007,7 @@ def rustc_compile_action( # Collect the linking contexts of the standard library and dependencies. linking_contexts = [ malloc_library[CcInfo].linking_context, - _get_std_and_alloc_info(ctx, toolchain, crate_info).linking_context, + _get_std_and_alloc_info(ctx, toolchain, crate_info, link_std_dylib).linking_context, toolchain.stdlib_linkflags.linking_context, ] @@ -2067,7 +2090,11 @@ def rustc_compile_action( runfiles = ctx.runfiles( files = getattr(ctx.files, "data", []) + - ([] if experimental_use_coverage_metadata_files else coverage_runfiles), + ([] if experimental_use_coverage_metadata_files else coverage_runfiles) + + # Include any generated Rust ABI dylibs as required runfiles. + ([crate_info.output] if crate_info.type == "dylib" else []) + + # Include the stdlib dylib when dynamically linking the standard library. + ([f for f in toolchain.rust_std.to_list() if is_std_dylib(f)] if link_std_dylib else []), ) transitive_runfiles = [] crate_attr = getattr(ctx.attr, "crate", None) @@ -2082,16 +2109,31 @@ def rustc_compile_action( continue for target in runfiles_attr: transitive_runfiles.append(target[DefaultInfo].default_runfiles) - if crate_info.type in ["bin", "cdylib", "staticlib"]: - dynamic_libraries = ctx.runfiles(files = [ + dep_dylib_files = [] + if crate_info.type in ["bin", "cdylib", "dylib", "staticlib"]: + dep_dylib_files = [ library_to_link.dynamic_library for dep in getattr(ctx.attr, "deps", []) + getattr(ctx.attr, "link_deps", []) if CcInfo in dep for linker_input in dep[CcInfo].linking_context.linker_inputs.to_list() for library_to_link in linker_input.libraries if _is_dylib(library_to_link) and library_to_link.dynamic_library - ]) - transitive_runfiles.append(dynamic_libraries) + ] + transitive_runfiles.append(ctx.runfiles(files = dep_dylib_files)) + + # On Windows there is no RPATH equivalent. Create symlinks of dylib files + # next to the binary so the Windows loader can find them. Bazel deduplicates + # identical symlink actions when multiple binaries in a package share a dylib dep. + if toolchain.target_os == "windows" and (crate_info.type == "bin" or crate_info.is_test): + win_dylibs = list(dep_dylib_files) + if link_std_dylib: + win_dylibs.extend([f for f in toolchain.rust_std.to_list() if is_std_dylib(f)]) + for dylib in win_dylibs: + if dylib.dirname != crate_info.output.dirname: + symlink = ctx.actions.declare_file(dylib.basename, sibling = crate_info.output) + ctx.actions.symlink(output = symlink, target_file = dylib) + outputs.append(symlink) + runfiles = runfiles.merge_all(transitive_runfiles) executable = crate_info.output if crate_info.type == "bin" or crate_info.is_test else None @@ -2197,10 +2239,12 @@ def _should_use_rustc_allocator_libraries(toolchain): return toolchain._experimental_use_allocator_libraries_with_mangled_symbols_setting return bool(use_or_default) -def _get_std_and_alloc_info(ctx, toolchain, crate_info): +def _get_std_and_alloc_info(ctx, toolchain, crate_info, link_std_dylib): # Handles standard libraries and allocator shims. # - # The standard libraries vary between "std" and "nostd" flavors. + # The standard libraries vary across two dimensions: + # * "std" vs "nostd" flavors, + # * dynamically vs statically linked. # # The allocator libraries vary along two dimensions: # * the type of rust allocator used (default or global) @@ -2210,6 +2254,9 @@ def _get_std_and_alloc_info(ctx, toolchain, crate_info): # # When provided, the allocator_libraries attribute takes precedence over the # toolchain allocator attributes. + if link_std_dylib and not toolchain.libstd_dylib_and_allocator_ccinfo: + fail("link_std_dylib was requested but no std dylib is available for this toolchain target.") + libs = None attr_allocator_library = None attr_global_allocator_library = None @@ -2219,7 +2266,11 @@ def _get_std_and_alloc_info(ctx, toolchain, crate_info): attr_global_allocator_library = libs.global_allocator_library if is_exec_configuration(ctx): if attr_allocator_library: + if link_std_dylib: + return libs.libstd_dylib_and_allocator_ccinfo return libs.libstd_and_allocator_ccinfo + if link_std_dylib: + return toolchain.libstd_dylib_and_allocator_ccinfo return toolchain.libstd_and_allocator_ccinfo if toolchain._experimental_use_global_allocator: if is_no_std(ctx, toolchain, crate_info.is_test): @@ -2228,10 +2279,18 @@ def _get_std_and_alloc_info(ctx, toolchain, crate_info): return toolchain.nostd_and_global_allocator_ccinfo else: if attr_global_allocator_library: + if link_std_dylib: + return libs.libstd_dylib_and_global_allocator_ccinfo return libs.libstd_and_global_allocator_ccinfo + if link_std_dylib: + return toolchain.libstd_dylib_and_global_allocator_ccinfo return toolchain.libstd_and_global_allocator_ccinfo if attr_allocator_library: + if link_std_dylib: + return libs.libstd_dylib_and_allocator_ccinfo return libs.libstd_and_allocator_ccinfo + if link_std_dylib: + return toolchain.libstd_dylib_and_allocator_ccinfo return toolchain.libstd_and_allocator_ccinfo def _is_dylib(dep): @@ -2322,7 +2381,7 @@ def establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_co return [] # Only generate CcInfo for particular crate types - if crate_info.type not in ("staticlib", "cdylib", "rlib", "lib"): + if crate_info.type not in ("dylib", "staticlib", "cdylib", "rlib", "lib"): return [] dot_a = None @@ -2372,6 +2431,14 @@ def establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_co dynamic_library = crate_info.output, interface_library = interface_library, ) + elif crate_info.type == "dylib": + if cc_toolchain: + library_to_link = cc_common.create_library_to_link( + actions = ctx.actions, + feature_configuration = feature_configuration, + cc_toolchain = cc_toolchain, + dynamic_library = crate_info.output, + ) else: fail("Unexpected case") @@ -2412,7 +2479,8 @@ def establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_co cc_infos.append(dep.cc_info) if crate_info.type in ("rlib", "lib"): - libstd_and_allocator_cc_info = _get_std_and_alloc_info(ctx, toolchain, crate_info) + # We're an rlib or lib, which uses the default toolchain setting for std dylib linking. + libstd_and_allocator_cc_info = _get_std_and_alloc_info(ctx, toolchain, crate_info, toolchain._link_std_dylib) if libstd_and_allocator_cc_info: # TODO: if we already have an rlib in our deps, we could skip this cc_infos.append(libstd_and_allocator_cc_info) @@ -2490,7 +2558,6 @@ def _process_build_scripts( if dep_build_info.out_dir: direct_inputs.append(dep_build_info.out_dir) transitive_inputs.append(dep_build_info.compile_data) - out_dir_compile_inputs = depset( direct_inputs, transitive = transitive_inputs, @@ -2503,7 +2570,7 @@ def _process_build_scripts( depset(build_flags_files, transitive = [dep_info.link_search_path_files]), ) -def _compute_rpaths(toolchain, output_dir, dep_info, use_pic): +def _compute_rpaths(toolchain, output_dir, dep_info, use_pic, link_std_dylib, output_file = None, workspace_name = ""): """Determine the artifact's rpaths relative to the bazel root for runtime linking of shared libraries. Args: @@ -2511,6 +2578,9 @@ def _compute_rpaths(toolchain, output_dir, dep_info, use_pic): output_dir (str): The output directory of the current target dep_info (DepInfo): The current target's dependency info use_pic: If set, prefers pic_static_library over static_library. + link_std_dylib (bool): If the current target should link the stdlib as a dynamic library. + output_file (File): The output binary file, used for runfiles-tree RPATHs. + workspace_name (str): The workspace name, used for runfiles-tree RPATHs. Returns: depset: A set of relative paths from the output directory to each dependency @@ -2529,7 +2599,7 @@ def _compute_rpaths(toolchain, output_dir, dep_info, use_pic): ] # Include std dylib if dylib linkage is enabled - if toolchain._experimental_link_std_dylib: + if link_std_dylib: # TODO: Make toolchain.rust_std to only include libstd.so # When dylib linkage is enabled, toolchain.rust_std should only need to # include libstd.so. Hence, no filtering needed. @@ -2550,11 +2620,48 @@ def _compute_rpaths(toolchain, output_dir, dep_info, use_pic): dep_info.transitive_noncrates, )) - # Multiple dylibs can be present in the same directory, so deduplicate them. - return depset([ + # RPATHs must cover three execution scenarios: + # + # A) Binary runs from exec root (local, sandboxed, or as symlink). + # $ORIGIN is the exec-root dir. Exec-root-relative RPATHs resolve. + # + # B) Binary runs from exec root but dylibs are only in the runfiles tree. + # $ORIGIN is the exec-root dir, and the runfiles tree is at + # $ORIGIN/.runfiles/. + # + # C) Binary runs from inside the runfiles tree. $ORIGIN is inside + # .runfiles//pkg/. Short-path-relative RPATHs + # navigate within the runfiles tree. + + # (A) Exec-root RPATHs. + rpaths = [ relativize(lib_dir, output_dir) for lib_dir in _get_dir_names(dylibs) - ]) + ] + + if output_file and workspace_name: + # (B) Runfiles-from-outside RPATHs. + runfiles_base = output_file.basename + ".runfiles" + runfiles_dirs = {} + for f in dylibs: + short_dir = paths.dirname(f.short_path) + if f.short_path.startswith("../"): + # External repo: short_path is "..//", runfiles + # tree places it at "/" at the runfiles root. + runfiles_dirs[paths.join(runfiles_base, short_dir[3:])] = None + else: + # Main repo: runfiles tree places it under "/". + runfiles_dirs[paths.join(runfiles_base, workspace_name, short_dir)] = None + rpaths.extend(runfiles_dirs.keys()) + + # (C) Short-path RPATHs (binary running inside runfiles tree). + output_short_dir = paths.dirname(output_file.short_path) + for f in dylibs: + rpath = relativize(paths.dirname(f.short_path), output_short_dir) + if rpath not in rpaths: + rpaths.append(rpath) + + return depset(rpaths) def _get_dir_names(files): """Returns a list of directory names from the given list of File objects diff --git a/rust/private/toolchain.bzl b/rust/private/toolchain.bzl index 0e313d460e..c45a350ce4 100644 --- a/rust/private/toolchain.bzl +++ b/rust/private/toolchain.bzl @@ -120,9 +120,14 @@ rust_stdlib_filegroup = rule( }, ) -def _experimental_link_std_dylib(ctx): +def _resolve_link_std_dylib(ctx): + per_toolchain = ctx.attr.link_std_dylib + if per_toolchain == -1: + enabled = ctx.attr._link_std_dylib_setting[BuildSettingInfo].value + else: + enabled = bool(per_toolchain) return not is_exec_configuration(ctx) and \ - ctx.attr.experimental_link_std_dylib[BuildSettingInfo].value and \ + enabled and \ ctx.attr.rust_std[rust_common.stdlib_info].std_dylib != None def _symlink_sysroot_tree(ctx, name, target, target_files = None): @@ -555,26 +560,25 @@ def _rust_toolchain_impl(ctx): ctx.label, )) - experimental_link_std_dylib = _experimental_link_std_dylib(ctx) - - def make_ccinfo(label, actions, allocator_library, std): + def make_ccinfo(label, actions, allocator_library, std, link_std_dylib): return make_libstd_and_allocator_ccinfo( cc_toolchain = cc_toolchain, feature_configuration = feature_configuration, label = label, actions = actions, - experimental_link_std_dylib = experimental_link_std_dylib, + link_std_dylib = link_std_dylib, rust_std = rust_std, allocator_library = allocator_library, std = std, ) - def make_local_ccinfo(allocator_library, std): + def make_local_ccinfo(allocator_library, std, link_std_dylib): return make_ccinfo( ctx.label, ctx.actions, struct(cc_info = allocator_library), std, + link_std_dylib, ) # Include C++ toolchain files to ensure tools like 'ar' are available for cross-compilation @@ -600,9 +604,11 @@ def _rust_toolchain_impl(ctx): env = ctx.attr.env, exec_triple = exec_triple, iso_date = ctx.attr.iso_date, - libstd_and_allocator_ccinfo = make_local_ccinfo(ctx.attr.allocator_library[CcInfo], "std"), - libstd_and_global_allocator_ccinfo = make_local_ccinfo(ctx.attr.global_allocator_library[CcInfo], "std"), - nostd_and_global_allocator_ccinfo = make_local_ccinfo(ctx.attr.global_allocator_library[CcInfo], "no_std_with_alloc"), + libstd_and_allocator_ccinfo = make_local_ccinfo(ctx.attr.allocator_library[CcInfo], "std", False), + libstd_and_global_allocator_ccinfo = make_local_ccinfo(ctx.attr.global_allocator_library[CcInfo], "std", False), + nostd_and_global_allocator_ccinfo = make_local_ccinfo(ctx.attr.global_allocator_library[CcInfo], "no_std_with_alloc", False), + libstd_dylib_and_allocator_ccinfo = make_local_ccinfo(ctx.attr.allocator_library[CcInfo], "std", True), + libstd_dylib_and_global_allocator_ccinfo = make_local_ccinfo(ctx.attr.global_allocator_library[CcInfo], "std", True), make_libstd_and_allocator_ccinfo = make_ccinfo, linker = sysroot.linker, linker_preference = linker_preference, @@ -643,7 +649,7 @@ def _rust_toolchain_impl(ctx): _rename_first_party_crates = rename_first_party_crates, _third_party_dir = third_party_dir, _pipelined_compilation = pipelined_compilation, - _experimental_link_std_dylib = _experimental_link_std_dylib(ctx), + _link_std_dylib = _resolve_link_std_dylib(ctx), _experimental_use_cc_common_link = _experimental_use_cc_common_link(ctx), _experimental_use_global_allocator = experimental_use_global_allocator, _experimental_compile_rustdoc_tests = ctx.attr._experimental_compile_rustdoc_tests[BuildSettingInfo].value, @@ -721,10 +727,6 @@ rust_toolchain = rule( ), mandatory = True, ), - "experimental_link_std_dylib": attr.label( - default = Label("@rules_rust//rust/settings:experimental_link_std_dylib"), - doc = "Label to a boolean build setting that controls whether whether to link libstd dynamically.", - ), "experimental_use_allocator_libraries_with_mangled_symbols": attr.int( doc = ( "Whether to use rust-based allocator libraries with " + @@ -914,6 +916,21 @@ rust_toolchain = rule( default = Label("//rust/settings:incompatible_do_not_include_transitive_data_in_compile_inputs"), doc = "Label to a boolean build setting that controls whether to include transitive data dependencies in compile inputs.", ), + "link_std_dylib": attr.int( + doc = ( + "Whether to link libstd dynamically. Possible values: [-1, 0, 1]. " + + "-1 means to use the value of the build setting " + + "//rust/settings:experimental_link_std_dylib. " + + "0 means do not link libstd dynamically. " + + "1 means link libstd dynamically." + ), + values = [-1, 0, 1], + default = -1, + ), + "_link_std_dylib_setting": attr.label( + default = Label("@rules_rust//rust/settings:experimental_link_std_dylib"), + doc = "Label to a boolean build setting that controls whether to link libstd dynamically.", + ), "_linker_preference": attr.label( default = Label("//rust/settings:toolchain_linker_preference"), ), diff --git a/rust/rust_cdylib_library.bzl b/rust/rust_cdylib_library.bzl new file mode 100644 index 0000000000..c0f757220e --- /dev/null +++ b/rust/rust_cdylib_library.bzl @@ -0,0 +1,8 @@ +"""rust_cdylib_library""" + +load( + "//rust/private:rust.bzl", + _rust_cdylib_library = "rust_cdylib_library", +) + +rust_cdylib_library = _rust_cdylib_library diff --git a/rust/rust_dylib_library.bzl b/rust/rust_dylib_library.bzl new file mode 100644 index 0000000000..ea3f40d005 --- /dev/null +++ b/rust/rust_dylib_library.bzl @@ -0,0 +1,8 @@ +"""rust_dylib_library""" + +load( + "//rust/private:rust.bzl", + _rust_dylib_library = "rust_dylib_library", +) + +rust_dylib_library = _rust_dylib_library diff --git a/rust/rust_shared_library.bzl b/rust/rust_shared_library.bzl index 665d85699e..2ef86f3df8 100644 --- a/rust/rust_shared_library.bzl +++ b/rust/rust_shared_library.bzl @@ -2,7 +2,7 @@ load( "//rust/private:rust.bzl", - _rust_shared_library = "rust_shared_library", + _rust_cdylib_library = "rust_cdylib_library", ) -rust_shared_library = _rust_shared_library +rust_shared_library = _rust_cdylib_library diff --git a/test/link_std_dylib/link_std_dylib_test.bzl b/test/link_std_dylib/link_std_dylib_test.bzl index f1338086d0..f35137a4e8 100644 --- a/test/link_std_dylib/link_std_dylib_test.bzl +++ b/test/link_std_dylib/link_std_dylib_test.bzl @@ -1,22 +1,20 @@ """Analysis tests for experimental_link_std_dylib flag""" load("@rules_cc//cc:defs.bzl", "CcInfo") -load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library") +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_dylib_library", "rust_library", "rust_test") load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") # buildifier: disable=bzl-visibility load("//rust/private:utils.bzl", "is_std_dylib") -def _test_rust_binary_impl(env, targets): - env.expect.that_action(targets.default_binary.actions[0]) \ +def _test_prefer_dynamic_impl(env, targets): + env.expect.that_action(targets.default_target.actions[0]) \ .contains_none_of_flag_values([ ("--codegen", "prefer-dynamic"), ]) - # Make sure with @rules_rust//rust/settings:experimental_link_std_dylib, - # the linker flags are set up correct so that the binary dynamically links - # the stdlib - env.expect.that_action(targets.binary_with_std_dylib.actions[0]) \ + # Make sure the target with std dylib linkage has the correct codegen flag + env.expect.that_action(targets.target_with_std_dylib.actions[0]) \ .contains_flag_values([ ("--codegen", "prefer-dynamic"), ]) @@ -31,13 +29,13 @@ def _test_rust_binary(name): analysis_test( name = name, - impl = _test_rust_binary_impl, + impl = _test_prefer_dynamic_impl, targets = { - "binary_with_std_dylib": name + "_rust_binary", - "default_binary": name + "_rust_binary", + "default_target": name + "_rust_binary", + "target_with_std_dylib": name + "_rust_binary", }, attrs = { - "binary_with_std_dylib": { + "target_with_std_dylib": { "@config_settings": { str(Label("@rules_rust//rust/settings:experimental_link_std_dylib")): True, }, @@ -45,6 +43,57 @@ def _test_rust_binary(name): }, ) +def _test_rust_binary_with_attr_dylib(name): + rust_binary( + name = name + "_rust_binary", + srcs = ["main.rs"], + edition = "2021", + tags = ["manual"], + ) + + rust_binary( + name = name + "_dylib_rust_binary", + srcs = ["main.rs"], + edition = "2021", + tags = ["manual"], + link_std_dylib = True, + ) + + analysis_test( + name = name, + impl = _test_prefer_dynamic_impl, + targets = { + "default_target": name + "_rust_binary", + "target_with_std_dylib": name + "_dylib_rust_binary", + }, + ) + +def _test_rust_dylib_with_attr(name): + rust_dylib_library( + name = name + "_rust_dylib_no_std_dylib", + srcs = ["lib.rs"], + edition = "2021", + tags = ["manual"], + link_std_dylib = False, + ) + + rust_dylib_library( + name = name + "_rust_dylib_link_std", + srcs = ["lib.rs"], + edition = "2021", + tags = ["manual"], + link_std_dylib = True, + ) + + analysis_test( + name = name, + impl = _test_prefer_dynamic_impl, + targets = { + "default_target": name + "_rust_dylib_no_std_dylib", + "target_with_std_dylib": name + "_rust_dylib_link_std", + }, + ) + def _export_static_stdlibs_in_cc_info(target): linker_inputs = target[CcInfo].linking_context.linker_inputs for linker_input in linker_inputs.to_list(): @@ -108,11 +157,167 @@ def _test_rust_library(name): }, ) +def _test_rust_test_with_attr(name): + rust_test( + name = name + "_rust_test", + srcs = ["main.rs"], + edition = "2021", + tags = ["manual"], + ) + + rust_test( + name = name + "_rust_test_link_std", + srcs = ["main.rs"], + edition = "2021", + tags = ["manual"], + link_std_dylib = True, + ) + + analysis_test( + name = name, + impl = _test_prefer_dynamic_impl, + targets = { + "default_target": name + "_rust_test", + "target_with_std_dylib": name + "_rust_test_link_std", + }, + ) + +def _has_runfiles_rpath(argv, binary_basename): + """Check that at least one link arg contains a path into the runfiles tree.""" + runfiles_marker = binary_basename + ".runfiles/" + for arg in argv: + if "link-arg=" in arg and runfiles_marker in arg: + return True + return False + +def _has_short_path_rpath(argv): + """Check that at least one link arg uses short-path-relative navigation (for inside-runfiles execution).""" + for arg in argv: + if "link-arg=" in arg and "../" in arg and ".runfiles" not in arg: + return True + return False + +def _test_runfiles_rpath_impl(env, targets): + action = targets.binary_with_dylib_dep.actions[0] + binary_basename = targets.binary_with_dylib_dep.label.name + + # Verify runfiles-from-outside RPATHs are present. + env.expect \ + .that_bool(_has_runfiles_rpath(action.argv, binary_basename)) \ + .equals(True) + +def _not_windows(): + return select({ + "@platforms//os:linux": [], + "@platforms//os:macos": [], + "//conditions:default": ["@platforms//:incompatible"], + }) + +def _test_runfiles_rpath(name): + rust_dylib_library( + name = name + "_rust_dylib", + srcs = ["lib.rs"], + edition = "2021", + tags = ["manual"], + target_compatible_with = _not_windows(), + ) + + rust_binary( + name = name + "_rust_binary", + srcs = ["main.rs"], + deps = [name + "_rust_dylib"], + edition = "2021", + tags = ["manual"], + target_compatible_with = _not_windows(), + ) + + analysis_test( + name = name, + impl = _test_runfiles_rpath_impl, + targets = { + "binary_with_dylib_dep": name + "_rust_binary", + }, + ) + +def _test_std_dylib_runfiles_rpath_impl(env, targets): + action = targets.binary_with_std_dylib.actions[0] + binary_basename = targets.binary_with_std_dylib.label.name + + # Verify runfiles-from-outside RPATHs for stdlib are present. + env.expect \ + .that_bool(_has_runfiles_rpath(action.argv, binary_basename)) \ + .equals(True) + + # Verify short-path RPATHs for stdlib are present. + # The stdlib is from an external repo, so its short-path RPATH uses "../". + env.expect \ + .that_bool(_has_short_path_rpath(action.argv)) \ + .equals(True) + +def _test_std_dylib_runfiles_rpath(name): + rust_binary( + name = name + "_rust_binary", + srcs = ["main.rs"], + edition = "2021", + link_std_dylib = True, + tags = ["manual"], + target_compatible_with = _not_windows(), + ) + + analysis_test( + name = name, + impl = _test_std_dylib_runfiles_rpath_impl, + targets = { + "binary_with_std_dylib": name + "_rust_binary", + }, + ) + +def _has_dylib_symlink_next_to_binary(target, dylib_dep): + """Check that a dylib output exists next to the binary in DefaultInfo.files.""" + dep_name = dylib_dep.label.name + for f in target[DefaultInfo].files.to_list(): + if dep_name in f.basename and (f.basename.endswith(".dll") or f.basename.endswith(".so") or f.basename.endswith(".dylib")): + return True + return False + +def _test_windows_dylib_symlink_impl(env, targets): + env.expect \ + .that_bool(_has_dylib_symlink_next_to_binary( + targets.binary_with_dylib_dep, + targets.dylib_dep, + )) \ + .equals(True) + +def _test_windows_dylib_symlink(name): + rust_binary( + name = name + "_rust_binary", + srcs = ["main.rs"], + deps = ["//test/rust_dylib:rust_dylib_lib"], + edition = "2021", + tags = ["manual"], + target_compatible_with = ["@platforms//os:windows"], + ) + + analysis_test( + name = name, + impl = _test_windows_dylib_symlink_impl, + targets = { + "binary_with_dylib_dep": name + "_rust_binary", + "dylib_dep": "//test/rust_dylib:rust_dylib_lib", + }, + ) + def link_std_dylib_test_suite(name): test_suite( name = name, tests = [ _test_rust_binary, _test_rust_library, + _test_rust_binary_with_attr_dylib, + _test_rust_dylib_with_attr, + _test_rust_test_with_attr, + _test_runfiles_rpath, + _test_std_dylib_runfiles_rpath, + _test_windows_dylib_symlink, ], ) diff --git a/test/rust_dylib/BUILD b/test/rust_dylib/BUILD new file mode 100644 index 0000000000..9e0d713b1b --- /dev/null +++ b/test/rust_dylib/BUILD @@ -0,0 +1,37 @@ +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_dylib_library", "rust_test") + +# Integration tests for rust_dylib_library: verifies that a binary depending +# on a Rust ABI dylib can be built and run, exercising runfiles inclusion +# and RPATH resolution end-to-end. + +NOT_WINDOWS = select({ + "@platforms//os:linux": [], + "@platforms//os:macos": [], + "//conditions:default": ["@platforms//:incompatible"], +}) + +rust_dylib_library( + name = "rust_dylib_lib", + srcs = ["lib.rs"], + edition = "2021", + link_std_dylib = True, + visibility = ["//test:__subpackages__"], +) + +rust_binary( + name = "dylib_dep_binary", + srcs = ["main.rs"], + deps = [":rust_dylib_lib"], + edition = "2021", + link_std_dylib = True, + target_compatible_with = NOT_WINDOWS, +) + +rust_test( + name = "dylib_dep_test", + srcs = ["main.rs"], + deps = [":rust_dylib_lib"], + edition = "2021", + link_std_dylib = True, + target_compatible_with = NOT_WINDOWS, +) diff --git a/test/rust_dylib/lib.rs b/test/rust_dylib/lib.rs new file mode 100644 index 0000000000..5252327577 --- /dev/null +++ b/test/rust_dylib/lib.rs @@ -0,0 +1,5 @@ +//! A library to be compiled as a dynamically linked library with the Rust ABI. + +pub fn example_test_dep_fn() -> u32 { + 1 +} diff --git a/test/rust_dylib/main.rs b/test/rust_dylib/main.rs new file mode 100644 index 0000000000..21633aafc0 --- /dev/null +++ b/test/rust_dylib/main.rs @@ -0,0 +1,9 @@ +//! An example binary that depends on a dynamically linked library. + +extern crate rust_dylib_lib; + +fn main() { + let val = rust_dylib_lib::example_test_dep_fn(); + assert_eq!(val, 1); + println!("dylib dep works: {}", val); +}