rustc: correctly handle alwayslink staticlibs (#606)
* test: add test that demonstrates missing alwayslink libraries
* refactor: inline get_libs_for_static_executable into rustc logic
We then move the function to its only remaining caller in bindgen. I
didn't inline it there because it's used in a few different ways and
cleaning it up felt like more work than it was immediately worth.
* refactor: make DepInfo carry around LibraryToLink and not File data
We need this information to implement support for alwayslink, and it
cascaded around the codebase more than a little. The next change will
implement always link, which is currently left as a stub so that this
change has no functional changes.
* rustc: remove awkward file-ending check for _is_dylib
Instead we just verify we're not going to link anything static. This
removes the need for a toolchain in _is_dylib, which will allow
further cleanups in a moment.
* rustc: remove unused toolchain= argument to _make_link_flags
This allows us to use a more efficient construction to add linker
arguments to the command line.
* rustc: add support for alwayslink library dependencies
This follows up on the previous commit's refactoring and actually adds
alwayslink support.
Fixes #325.
* tests: add coverage for a cdylib with alwayslink
Makes sure that cdylibs also correctly pick up their alwayslink
dependencies.
* rustc: tote around LinkerInput for noncrate deps
This lets us avoid reifying a depset a little longer, which is nice.
Co-authored-by: Augie Fackler <augie@google.com>
diff --git a/bindgen/bindgen.bzl b/bindgen/bindgen.bzl
index 7644f78..9265f9f 100644
--- a/bindgen/bindgen.bzl
+++ b/bindgen/bindgen.bzl
@@ -16,7 +16,20 @@
load("//rust:rust.bzl", "rust_library")
# buildifier: disable=bzl-visibility
-load("//rust/private:utils.bzl", "find_toolchain", "get_libs_for_static_executable")
+load("//rust/private:utils.bzl", "find_toolchain", "get_preferred_artifact")
+
+# TODO(hlopko): use the more robust logic from rustc.bzl also here, through a reasonable API.
+def _get_libs_for_static_executable(dep):
+ """find the libraries used for linking a static executable.
+
+ Args:
+ dep (Target): A cc_library target.
+
+ Returns:
+ depset: A depset[File]
+ """
+ linker_inputs = dep[CcInfo].linking_context.linker_inputs.to_list()
+ return depset([get_preferred_artifact(lib) for li in linker_inputs for lib in li.libraries])
def rust_bindgen_library(
name,
@@ -84,7 +97,7 @@
output = ctx.outputs.out
# libclang should only have 1 output file
- libclang_dir = get_libs_for_static_executable(libclang).to_list()[0].dirname
+ libclang_dir = _get_libs_for_static_executable(libclang).to_list()[0].dirname
include_directories = cc_lib[CcInfo].compilation_context.includes.to_list()
quote_include_directories = cc_lib[CcInfo].compilation_context.quote_includes.to_list()
system_include_directories = cc_lib[CcInfo].compilation_context.system_includes.to_list()
@@ -112,7 +125,7 @@
}
if libstdcxx:
- env["LD_LIBRARY_PATH"] = ":".join([f.dirname for f in get_libs_for_static_executable(libstdcxx).to_list()])
+ env["LD_LIBRARY_PATH"] = ":".join([f.dirname for f in _get_libs_for_static_executable(libstdcxx).to_list()])
ctx.actions.run(
executable = bindgen_bin,
@@ -120,9 +133,9 @@
[header],
transitive = [
cc_lib[CcInfo].compilation_context.headers,
- get_libs_for_static_executable(libclang),
+ _get_libs_for_static_executable(libclang),
] + [
- get_libs_for_static_executable(libstdcxx),
+ _get_libs_for_static_executable(libstdcxx),
] if libstdcxx else [],
),
outputs = [unformatted_output],
diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl
index 7686edd..d71c629 100644
--- a/rust/private/rustc.bzl
+++ b/rust/private/rustc.bzl
@@ -23,7 +23,7 @@
"expand_locations",
"find_cc_toolchain",
"get_lib_name",
- "get_libs_for_static_executable",
+ "get_preferred_artifact",
"relativize",
"rule_attrs",
)
@@ -54,9 +54,8 @@
"direct_crates": "depset[CrateInfo]",
"transitive_build_infos": "depset[BuildInfo]",
"transitive_crates": "depset[CrateInfo]",
- "transitive_dylibs": "depset[File]",
"transitive_libs": "List[File]: All transitive dependencies, not filtered by type.",
- "transitive_staticlibs": "depset[File]",
+ "transitive_noncrates": "depset[LinkerInput]: All transitive dependencies that aren't crates.",
},
)
@@ -149,8 +148,8 @@
direct_crates = []
transitive_crates = []
- transitive_dylibs = []
- transitive_staticlibs = []
+ transitive_noncrates = []
+ transitive_noncrate_libs = []
transitive_build_infos = []
build_info = None
@@ -165,26 +164,17 @@
))
transitive_crates.append(depset([dep[rust_common.crate_info]], transitive = [dep[DepInfo].transitive_crates]))
- transitive_dylibs.append(dep[DepInfo].transitive_dylibs)
- transitive_staticlibs.append(dep[DepInfo].transitive_staticlibs)
+ transitive_noncrates.append(dep[DepInfo].transitive_noncrates)
+ transitive_noncrate_libs.append(depset(dep[DepInfo].transitive_libs))
transitive_build_infos.append(dep[DepInfo].transitive_build_infos)
elif CcInfo in dep:
# This dependency is a cc_library
# TODO: We could let the user choose how to link, instead of always preferring to link static libraries.
- libs = get_libs_for_static_executable(dep)
-
- transitive_dylibs.append(depset([
- lib
- for lib in libs.to_list()
- # Dynamic libraries may have a version number nowhere, or before (macos) or after (linux) the extension.
- if lib.basename.endswith(toolchain.dylib_ext) or lib.basename.split(".", 2)[1] == toolchain.dylib_ext[1:]
- ]))
- transitive_staticlibs.append(depset([
- lib
- for lib in libs.to_list()
- if lib.basename.endswith(toolchain.staticlib_ext)
- ]))
+ linker_inputs = dep[CcInfo].linking_context.linker_inputs.to_list()
+ libs = [get_preferred_artifact(lib) for li in linker_inputs for lib in li.libraries]
+ transitive_noncrate_libs.append(depset(libs))
+ transitive_noncrates.append(depset(dep[CcInfo].linking_context.linker_inputs))
elif BuildInfo in dep:
if build_info:
fail("Several deps are providing build information, only one is allowed in the dependencies", "deps")
@@ -196,18 +186,17 @@
transitive_crates_depset = depset(transitive = transitive_crates)
transitive_libs = depset(
[c.output for c in transitive_crates_depset.to_list()],
- transitive = transitive_staticlibs + transitive_dylibs,
+ transitive = transitive_noncrate_libs,
)
return (
DepInfo(
direct_crates = depset(direct_crates),
transitive_crates = transitive_crates_depset,
- transitive_dylibs = depset(
- transitive = transitive_dylibs,
+ transitive_noncrates = depset(
+ transitive = transitive_noncrates,
order = "topological", # dylib link flag ordering matters.
),
- transitive_staticlibs = depset(transitive = transitive_staticlibs),
transitive_libs = transitive_libs.to_list(),
transitive_build_infos = depset(transitive = transitive_build_infos),
dep_env = build_info.dep_env if build_info else None,
@@ -498,7 +487,7 @@
args.add("--codegen=linker=" + ld)
args.add_joined("--codegen", link_args, join_with = " ", format_joined = "link-args=%s")
- _add_native_link_flags(args, dep_info, crate_type, cc_toolchain, feature_configuration)
+ _add_native_link_flags(args, dep_info, crate_type, toolchain, cc_toolchain, feature_configuration)
# These always need to be added, even if not linking this crate.
add_crate_link_flags(args, dep_info)
@@ -611,8 +600,10 @@
),
)
+ dylibs = [get_preferred_artifact(lib) for linker_input in dep_info.transitive_noncrates.to_list() for lib in linker_input.libraries if _is_dylib(lib)]
+
runfiles = ctx.runfiles(
- files = dep_info.transitive_dylibs.to_list() + getattr(ctx.files, "data", []),
+ files = dylibs + getattr(ctx.files, "data", []),
collect_data = True,
)
@@ -631,6 +622,9 @@
),
]
+def _is_dylib(dep):
+ return not bool(dep.static_library or dep.pic_static_library)
+
def establish_cc_info(ctx, crate_info, toolchain, cc_toolchain, feature_configuration):
"""If the produced crate is suitable yield a CcInfo to allow for interop with cc rules
@@ -754,18 +748,24 @@
Returns:
depset: A set of relative paths from the output directory to each dependency
"""
- if not dep_info.transitive_dylibs:
+ preferreds = [get_preferred_artifact(lib) for linker_input in dep_info.transitive_noncrates.to_list() for lib in linker_input.libraries]
+
+ # TODO(augie): I don't understand why this can't just be filtering on
+ # _is_dylib(lib), but doing that causes failures on darwin and windows
+ # examples that otherwise work.
+ dylibs = [lib for lib in preferreds if lib.basename.endswith(toolchain.dylib_ext) or lib.basename.split(".", 2)[1] == toolchain.dylib_ext[1:]]
+ if not dylibs:
return depset([])
if toolchain.os != "linux":
fail("Runtime linking is not supported on {}, but found {}".format(
toolchain.os,
- dep_info.transitive_dylibs,
+ dep_info.transitive_noncrates,
))
# Multiple dylibs can be present in the same directory, so deduplicate them.
return depset([
relativize(lib_dir, output_dir)
- for lib_dir in _get_dir_names(dep_info.transitive_dylibs.to_list())
+ for lib_dir in _get_dir_names(dylibs)
])
def _get_dir_names(files):
@@ -821,25 +821,78 @@
"""
return crate.output.dirname
-def _add_native_link_flags(args, dep_info, crate_type, cc_toolchain, feature_configuration):
+def _portable_link_flags(lib):
+ if lib.static_library or lib.pic_static_library:
+ return ["-lstatic=%s" % get_lib_name(get_preferred_artifact(lib))]
+ elif _is_dylib(lib):
+ return ["-ldylib=%s" % get_lib_name(get_preferred_artifact(lib))]
+ return []
+
+def _make_link_flags_windows(linker_input):
+ ret = []
+ for lib in linker_input.libraries:
+ if lib.alwayslink:
+ ret.extend(["-C", "link-arg=/WHOLEARCHIVE:%s" % get_preferred_artifact(lib).path])
+ else:
+ ret.extend(_portable_link_flags(lib))
+ return ret
+
+def _make_link_flags_darwin(linker_input):
+ ret = []
+ for lib in linker_input.libraries:
+ if lib.alwayslink:
+ ret.extend([
+ "-C",
+ ("link-arg=-Wl,-force_load,%s" % get_preferred_artifact(lib).path),
+ ])
+ else:
+ ret.extend(_portable_link_flags(lib))
+ return ret
+
+def _make_link_flags_default(linker_input):
+ ret = []
+ for lib in linker_input.libraries:
+ if lib.alwayslink:
+ ret.extend([
+ "-C",
+ "link-arg=-Wl,--whole-archive",
+ "-C",
+ ("link-arg=%s" % get_preferred_artifact(lib).path),
+ "-C",
+ "link-arg=-Wl,--no-whole-archive",
+ ])
+ else:
+ ret.extend(_portable_link_flags(lib))
+ return ret
+
+def _libraries_dirnames(linker_input):
+ return [get_preferred_artifact(lib).dirname for lib in linker_input.libraries]
+
+def _add_native_link_flags(args, dep_info, crate_type, toolchain, cc_toolchain, feature_configuration):
"""Adds linker flags for all dependencies of the current target.
Args:
args (Args): The Args struct for a ctx.action
dep_info (DepInfo): Dependency Info provider
crate_type: Crate type of the current target
+ toolchain (rust_toolchain): The current `rust_toolchain`
cc_toolchain (CcToolchainInfo): The current `cc_toolchain`
feature_configuration (FeatureConfiguration): feature configuration to use with cc_toolchain
"""
- native_libs = depset(transitive = [dep_info.transitive_dylibs, dep_info.transitive_staticlibs])
- args.add_all(native_libs, map_each = _get_dirname, uniquify = True, format_each = "-Lnative=%s")
+ args.add_all(dep_info.transitive_noncrates, map_each = _libraries_dirnames, uniquify = True, format_each = "-Lnative=%s")
if crate_type in ["lib", "rlib"]:
return
- args.add_all(dep_info.transitive_dylibs, map_each = get_lib_name, format_each = "-ldylib=%s")
- args.add_all(dep_info.transitive_staticlibs, map_each = get_lib_name, format_each = "-lstatic=%s")
+ if toolchain.os == "windows":
+ make_link_flags = _make_link_flags_windows
+ elif toolchain.os.startswith("mac") or toolchain.os.startswith("darwin"):
+ make_link_flags = _make_link_flags_darwin
+ else:
+ make_link_flags = _make_link_flags_default
+
+ args.add_all(dep_info.transitive_noncrates, map_each = make_link_flags)
if crate_type in ["dylib", "cdylib"]:
# For shared libraries we want to link C++ runtime library dynamically
diff --git a/rust/private/rustdoc_test.bzl b/rust/private/rustdoc_test.bzl
index fbaf7d5..b407900 100644
--- a/rust/private/rustdoc_test.bzl
+++ b/rust/private/rustdoc_test.bzl
@@ -15,7 +15,7 @@
# buildifier: disable=module-docstring
load("//rust/private:common.bzl", "rust_common")
load("//rust/private:rustc.bzl", "DepInfo")
-load("//rust/private:utils.bzl", "find_toolchain", "get_lib_name")
+load("//rust/private:utils.bzl", "find_toolchain", "get_lib_name", "get_preferred_artifact")
def _rust_doc_test_impl(ctx):
"""The implementation for the `rust_doc_test` rule
@@ -79,7 +79,7 @@
return "/".join(path_str.split("/")[:-1])
def _build_rustdoc_flags(dep_info, crate):
- """Constructs the rustdoc script used to test `crate`.
+ """Constructs the rustdoc script used to test `crate`.
Args:
dep_info (DepInfo): The DepInfo provider
@@ -99,10 +99,16 @@
link_flags += ["--extern=" + c.name + "=" + c.dep.output.short_path for c in d.direct_crates.to_list()]
link_search_flags += ["-Ldependency={}".format(_dirname(c.output.short_path)) for c in d.transitive_crates.to_list()]
- link_flags += ["-ldylib=" + get_lib_name(lib) for lib in d.transitive_dylibs.to_list()]
- link_search_flags += ["-Lnative={}".format(_dirname(lib.short_path)) for lib in d.transitive_dylibs.to_list()]
- link_flags += ["-lstatic=" + get_lib_name(lib) for lib in d.transitive_staticlibs.to_list()]
- link_search_flags += ["-Lnative={}".format(_dirname(lib.short_path)) for lib in d.transitive_staticlibs.to_list()]
+ # TODO(hlopko): use the more robust logic from rustc.bzl also here, through a reasonable API.
+ for lib_to_link in dep_info.transitive_noncrates.to_list():
+ is_static = bool(lib_to_link.static_library or lib_to_link.pic_static_library)
+ f = get_preferred_artifact(lib_to_link)
+ if not is_static:
+ link_flags.append("-ldylib=" + get_lib_name(f))
+ else:
+ link_flags.append("-lstatic=" + get_lib_name(f))
+ link_flags.append("-Lnative={}".format(_dirname(f.short_path)))
+ link_search_flags.append("-Lnative={}".format(_dirname(f.short_path)))
edition_flags = ["--edition={}".format(crate.edition)] if crate.edition != "2015" else []
diff --git a/rust/private/utils.bzl b/rust/private/utils.bzl
index 5bd6259..06ed842 100644
--- a/rust/private/utils.bzl
+++ b/rust/private/utils.bzl
@@ -118,19 +118,7 @@
"""
return repr(hash(crate_root.path))
-def get_libs_for_static_executable(dep):
- """find the libraries used for linking a static executable.
-
- Args:
- dep (Target): A cc_library target.
-
- Returns:
- depset: A depset[File]
- """
- linker_inputs = dep[CcInfo].linking_context.linker_inputs.to_list()
- return depset([_get_preferred_artifact(lib) for li in linker_inputs for lib in li.libraries])
-
-def _get_preferred_artifact(library_to_link):
+def get_preferred_artifact(library_to_link):
"""Get the first available library to link from a LibraryToLink object.
Args:
diff --git a/test/unit/native_deps/alwayslink.cc b/test/unit/native_deps/alwayslink.cc
new file mode 100644
index 0000000..bd98ce0
--- /dev/null
+++ b/test/unit/native_deps/alwayslink.cc
@@ -0,0 +1 @@
+extern "C" int alwayslink() { return 42; }
diff --git a/test/unit/native_deps/native_deps_test.bzl b/test/unit/native_deps/native_deps_test.bzl
index 8086d74..5803f77 100644
--- a/test/unit/native_deps/native_deps_test.bzl
+++ b/test/unit/native_deps/native_deps_test.bzl
@@ -28,7 +28,7 @@
prefix = prefix,
suffix = suffix,
args = action.argv,
- )
+ ),
)
def _lib_has_no_native_libs_test_impl(ctx):
@@ -103,11 +103,75 @@
_assert_argv_contains(env, action, "-lstatic=native_dep")
return analysistest.end(env)
+def _extract_linker_args(argv):
+ return [a for a in argv if a.startswith("link-arg=") or a.startswith("link-arg=-l") or a.startswith("-l") or a.endswith(".lo") or a.endswith(".o")]
+
+def _bin_has_native_dep_and_alwayslink_test_impl(ctx):
+ env = analysistest.begin(ctx)
+ tut = analysistest.target_under_test(env)
+ actions = analysistest.target_actions(env)
+ action = actions[0]
+
+ if ctx.target_platform_has_constraint(ctx.attr._macos_constraint[platform_common.ConstraintValueInfo]):
+ want = [
+ "-lstatic=native_dep",
+ "link-arg=-Wl,-force_load,bazel-out/darwin-fastbuild/bin/test/unit/native_deps/libalwayslink.lo",
+ ]
+ elif ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]):
+ want = [
+ "-lstatic=native_dep",
+ "link-arg=/WHOLEARCHIVE:bazel-out/x64_windows-fastbuild/bin/test/unit/native_deps/alwayslink.lo.lib",
+ ]
+ else:
+ want = [
+ "-lstatic=native_dep",
+ "link-arg=-Wl,--whole-archive",
+ "link-arg=bazel-out/k8-fastbuild/bin/test/unit/native_deps/libalwayslink.lo",
+ "link-arg=-Wl,--no-whole-archive",
+ ]
+ asserts.equals(env, want, _extract_linker_args(action.argv))
+ return analysistest.end(env)
+
+def _cdylib_has_native_dep_and_alwayslink_test_impl(ctx):
+ env = analysistest.begin(ctx)
+ tut = analysistest.target_under_test(env)
+ actions = analysistest.target_actions(env)
+ action = actions[0]
+
+ if ctx.target_platform_has_constraint(ctx.attr._macos_constraint[platform_common.ConstraintValueInfo]):
+ want = [
+ "-lstatic=native_dep",
+ "link-arg=-Wl,-force_load,bazel-out/darwin-fastbuild/bin/test/unit/native_deps/libalwayslink.lo",
+ ]
+ elif ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]):
+ want = [
+ "-lstatic=native_dep",
+ "link-arg=/WHOLEARCHIVE:bazel-out/x64_windows-fastbuild/bin/test/unit/native_deps/alwayslink.lo.lib",
+ ]
+ else:
+ want = [
+ "-lstatic=native_dep",
+ "link-arg=-Wl,--whole-archive",
+ "link-arg=bazel-out/k8-fastbuild/bin/test/unit/native_deps/libalwayslink.lo",
+ "link-arg=-Wl,--no-whole-archive",
+ ]
+ asserts.equals(env, want, _extract_linker_args(action.argv))
+ return analysistest.end(env)
+
+
rlib_has_no_native_libs_test = analysistest.make(_rlib_has_no_native_libs_test_impl)
staticlib_has_native_libs_test = analysistest.make(_staticlib_has_native_libs_test_impl)
cdylib_has_native_libs_test = analysistest.make(_cdylib_has_native_libs_test_impl)
proc_macro_has_native_libs_test = analysistest.make(_proc_macro_has_native_libs_test_impl)
bin_has_native_libs_test = analysistest.make(_bin_has_native_libs_test_impl)
+bin_has_native_dep_and_alwayslink_test = analysistest.make(_bin_has_native_dep_and_alwayslink_test_impl, attrs = {
+ "_macos_constraint": attr.label(default = Label("@platforms//os:macos")),
+ "_windows_constraint": attr.label(default = Label("@platforms//os:windows")),
+})
+cdylib_has_native_dep_and_alwayslink_test = analysistest.make(_cdylib_has_native_libs_test_impl, attrs = {
+ "_macos_constraint": attr.label(default = Label("@platforms//os:macos")),
+ "_windows_constraint": attr.label(default = Label("@platforms//os:windows")),
+})
def _native_dep_test():
rust_library(
@@ -141,11 +205,29 @@
deps = [":native_dep"],
)
+ rust_binary(
+ name = "bin_has_native_dep_and_alwayslink",
+ srcs = ["bin_using_native_dep.rs"],
+ deps = [":native_dep", ":alwayslink"],
+ )
+
cc_library(
name = "native_dep",
srcs = ["native_dep.cc"],
)
+ cc_library(
+ name = "alwayslink",
+ srcs = ["alwayslink.cc"],
+ alwayslink = 1,
+ )
+
+ rust_shared_library(
+ name = "cdylib_has_native_dep_and_alwayslink",
+ srcs = ["lib_using_native_dep.rs"],
+ deps = [":native_dep", ":alwayslink"],
+ )
+
rlib_has_no_native_libs_test(
name = "rlib_has_no_native_libs_test",
target_under_test = ":rlib_has_no_native_dep",
@@ -166,6 +248,14 @@
name = "bin_has_native_libs_test",
target_under_test = ":bin_has_native_dep",
)
+ bin_has_native_dep_and_alwayslink_test(
+ name = "bin_has_native_dep_and_alwayslink_test",
+ target_under_test = ":bin_has_native_dep_and_alwayslink",
+ )
+ cdylib_has_native_dep_and_alwayslink_test(
+ name = "cdylib_has_native_dep_and_alwayslink_test",
+ target_under_test = ":cdylib_has_native_dep_and_alwayslink",
+ )
def native_deps_test_suite(name):
"""Entry-point macro called from the BUILD file.
@@ -183,5 +273,7 @@
":cdylib_has_native_libs_test",
":proc_macro_has_native_libs_test",
":bin_has_native_libs_test",
+ ":bin_has_native_dep_and_alwayslink_test",
+ ":cdylib_has_native_dep_and_alwayslink_test",
],
)