Skip to content

Commit b45fd8f

Browse files
committed
Handle LTO with an rlib/cdylib crate type
In the case that LTO is happening but we're also generating a cdylib/rlib simultatneously that means that the final artifact will use the rlib but the cdylib still needs to be produced. To get this to work the cdylib's dependency tree needs to be compiled with embedded bitcode. The cdylib itself will be linked with the linker because we can't LTO an rlib+cdylib compilation, but the final executable will need to load bitcode from all its deps. This is meant to address rust-lang/rust#72268
1 parent 13bded9 commit b45fd8f

File tree

3 files changed

+115
-37
lines changed

3 files changed

+115
-37
lines changed

src/cargo/core/compiler/lto.rs

+40-26
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::core::compiler::{Context, Unit};
22
use crate::core::interning::InternedString;
33
use crate::core::profiles;
4+
use crate::core::TargetKind;
45
use crate::util::errors::CargoResult;
56
use std::collections::hash_map::{Entry, HashMap};
67

@@ -26,7 +27,7 @@ pub enum Lto {
2627
pub fn generate(cx: &mut Context<'_, '_>) -> CargoResult<()> {
2728
let mut map = HashMap::new();
2829
for unit in cx.bcx.roots.iter() {
29-
calculate(cx, &mut map, unit, false)?;
30+
calculate(cx, &mut map, unit, Lto::None)?;
3031
}
3132
cx.lto = map;
3233
Ok(())
@@ -36,34 +37,49 @@ fn calculate(
3637
cx: &Context<'_, '_>,
3738
map: &mut HashMap<Unit, Lto>,
3839
unit: &Unit,
39-
require_bitcode: bool,
40+
lto_for_deps: Lto,
4041
) -> CargoResult<()> {
41-
let (lto, require_bitcode_for_deps) = if unit.target.for_host() {
42+
let (lto, lto_for_deps) = if unit.target.for_host() {
4243
// Disable LTO for host builds since we only really want to perform LTO
4344
// for the final binary, and LTO on plugins/build scripts/proc macros is
4445
// largely not desired.
45-
(Lto::None, false)
46-
} else if unit.target.can_lto() {
47-
// Otherwise if this target can perform LTO then we're going to read the
48-
// LTO value out of the profile. Note that we ignore `require_bitcode`
46+
(Lto::None, Lto::None)
47+
} else if unit.target.is_linkable() {
48+
// A "linkable" target is one that produces and rlib or dylib in this
49+
// case. In this scenario we cannot pass `-Clto` to the compiler because
50+
// that is an invalid request, this is simply a dependency. What we do,
51+
// however, is respect the request for whatever dependencies need to
52+
// have.
53+
//
54+
// Here if no LTO is requested then we keep it turned off. Otherwise LTO
55+
// is requested in some form, which means ideally we need just what's
56+
// requested, nothing else. It's possible, though, to have libraries
57+
// which are both a cdylib and and rlib, for example, which means that
58+
// object files are getting sent to the linker. That means that we need
59+
// to fully embed bitcode rather than simply generating just bitcode.
60+
let has_non_linkable_lib = match unit.target.kind() {
61+
TargetKind::Lib(kinds) => kinds.iter().any(|k| !k.is_linkable()),
62+
_ => true,
63+
};
64+
match lto_for_deps {
65+
Lto::None => (Lto::None, Lto::None),
66+
_ if has_non_linkable_lib => (Lto::EmbedBitcode, Lto::EmbedBitcode),
67+
other => (other, other),
68+
}
69+
} else {
70+
// Otherwise this target can perform LTO and we're going to read the
71+
// LTO value out of the profile. Note that we ignore `lto_for_deps`
4972
// here because if a unit depends on another unit than can LTO this
5073
// isn't a rustc-level dependency but rather a Cargo-level dependency.
5174
// For example this is an integration test depending on a binary.
5275
match unit.profile.lto {
5376
profiles::Lto::Named(s) => match s.as_str() {
54-
"n" | "no" | "off" => (Lto::Run(Some(s)), false),
55-
_ => (Lto::Run(Some(s)), true),
77+
"n" | "no" | "off" => (Lto::Run(Some(s)), Lto::None),
78+
_ => (Lto::Run(Some(s)), Lto::OnlyBitcode),
5679
},
57-
profiles::Lto::Bool(true) => (Lto::Run(None), true),
58-
profiles::Lto::Bool(false) => (Lto::None, false),
80+
profiles::Lto::Bool(true) => (Lto::Run(None), Lto::OnlyBitcode),
81+
profiles::Lto::Bool(false) => (Lto::None, Lto::None),
5982
}
60-
} else if require_bitcode {
61-
// Otherwise we're a dependency of something, an rlib. This means that
62-
// if our parent required bitcode of some kind then we need to generate
63-
// bitcode.
64-
(Lto::OnlyBitcode, true)
65-
} else {
66-
(Lto::None, false)
6783
};
6884

6985
match map.entry(unit.clone()) {
@@ -93,14 +109,12 @@ fn calculate(
93109
// either only-objects or only-bitcode we have to embed both in
94110
// rlibs (used for different compilations), so we switch to
95111
// embedding bitcode.
96-
(Lto::OnlyBitcode, Lto::None)
97-
| (Lto::OnlyBitcode, Lto::EmbedBitcode)
98-
| (Lto::None, Lto::OnlyBitcode)
99-
| (Lto::None, Lto::EmbedBitcode) => Lto::EmbedBitcode,
112+
(Lto::OnlyBitcode, Lto::None) | (Lto::None, Lto::OnlyBitcode) => Lto::EmbedBitcode,
100113

101-
// Currently this variant is never calculated above, so no need
102-
// to handle this case.
103-
(Lto::EmbedBitcode, _) => unreachable!(),
114+
// Once a target has requested bitcode embedding that's the
115+
// maximal amount of work that can be done, so we just keep
116+
// doing that work.
117+
(Lto::EmbedBitcode, _) | (_, Lto::EmbedBitcode) => Lto::EmbedBitcode,
104118
};
105119
// No need to recurse if we calculated the same value as before.
106120
if result == *v.get() {
@@ -111,7 +125,7 @@ fn calculate(
111125
}
112126

113127
for dep in cx.unit_deps(unit) {
114-
calculate(cx, map, &dep.unit, require_bitcode_for_deps)?;
128+
calculate(cx, map, &dep.unit, lto_for_deps)?;
115129
}
116130
Ok(())
117131
}

src/cargo/core/manifest.rs

-11
Original file line numberDiff line numberDiff line change
@@ -842,17 +842,6 @@ impl Target {
842842
self.kind().rustc_crate_types()
843843
}
844844

845-
pub fn can_lto(&self) -> bool {
846-
match self.kind() {
847-
TargetKind::Lib(v) => {
848-
!v.contains(&CrateType::Rlib)
849-
&& !v.contains(&CrateType::Dylib)
850-
&& !v.contains(&CrateType::Lib)
851-
}
852-
_ => true,
853-
}
854-
}
855-
856845
pub fn set_tested(&mut self, tested: bool) -> &mut Target {
857846
Arc::make_mut(&mut self.inner).tested = tested;
858847
self

tests/testsuite/lto.rs

+75
Original file line numberDiff line numberDiff line change
@@ -336,3 +336,78 @@ fn test_all_and_bench() {
336336
.with_stderr_contains("[RUNNING] `rustc[..]--crate-name foo[..]-C lto[..]")
337337
.run();
338338
}
339+
340+
#[cargo_test]
341+
fn cdylib_and_rlib() {
342+
if !cargo_test_support::is_nightly() {
343+
return;
344+
}
345+
346+
Package::new("registry", "0.0.1")
347+
.file("src/lib.rs", "pub fn foo() {}")
348+
.publish();
349+
Package::new("registry-shared", "0.0.1")
350+
.file("src/lib.rs", "pub fn foo() {}")
351+
.publish();
352+
353+
let p = project()
354+
.file(
355+
"Cargo.toml",
356+
r#"
357+
[package]
358+
name = "foo"
359+
version = "0.0.0"
360+
361+
[workspace]
362+
363+
[dependencies]
364+
bar = { path = 'bar' }
365+
registry-shared = "*"
366+
367+
[profile.release]
368+
lto = true
369+
"#,
370+
)
371+
.file(
372+
"src/main.rs",
373+
"
374+
fn main() {
375+
bar::foo();
376+
registry_shared::foo();
377+
}
378+
",
379+
)
380+
.file(
381+
"bar/Cargo.toml",
382+
r#"
383+
[package]
384+
name = "bar"
385+
version = "0.0.0"
386+
387+
[dependencies]
388+
registry = "*"
389+
registry-shared = "*"
390+
391+
[lib]
392+
crate-type = ['cdylib', 'rlib']
393+
"#,
394+
)
395+
.file(
396+
"bar/src/lib.rs",
397+
"
398+
pub fn foo() {
399+
registry::foo();
400+
registry_shared::foo();
401+
}
402+
",
403+
)
404+
.file("tests/a.rs", "")
405+
.file("bar/tests/b.rs", "")
406+
.build();
407+
p.cargo("build --release -v").run();
408+
p.cargo("test --release -v").run();
409+
p.cargo("build --release -v --manifest-path bar/Cargo.toml")
410+
.run();
411+
p.cargo("test --release -v --manifest-path bar/Cargo.toml")
412+
.run();
413+
}

0 commit comments

Comments
 (0)