mod library;
mod near;
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use crate::args::LinkOptions;
use crate::diag::Diagnostic;
use crate::input::search::apply_sysroot;
pub use library::{Definition, Entry, LibraryIndex, LibraryKind, Object};
pub use near::{MAX_NEAR_MISSES, NearMiss, NearMissKind, near_misses};
pub const MAX_LIBRARIES: usize = 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Undefined<'a> {
pub name: &'a [u8],
pub version: Option<&'a [u8]>,
}
impl<'a> Undefined<'a> {
#[must_use]
pub fn new(name: &'a [u8]) -> Self {
Self {
name,
version: None,
}
}
#[must_use]
pub fn versioned(name: &'a [u8], version: &'a [u8]) -> Self {
Self {
name,
version: Some(version),
}
}
#[must_use]
pub fn parse(text: &'a [u8]) -> Self {
match text.iter().position(|&c| c == b'@') {
Some(at) => {
let name = text.get(..at).unwrap_or_default();
let rest = text.get(at.saturating_add(1)..).unwrap_or_default();
let version = rest.strip_prefix(b"@").unwrap_or(rest);
Self {
name,
version: (!version.is_empty()).then_some(version),
}
}
None => Self::new(text),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LinkedLibrary {
pub path: PathBuf,
pub dropped_as_needed: bool,
pub static_only: bool,
}
impl LinkedLibrary {
#[must_use]
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
dropped_as_needed: false,
static_only: false,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SearchScope {
pub search_paths: Vec<PathBuf>,
pub sysroot: Option<PathBuf>,
}
impl SearchScope {
#[must_use]
pub fn from_options(options: &LinkOptions) -> Self {
Self {
search_paths: options.search_paths.clone(),
sysroot: options.sysroot.clone(),
}
}
#[must_use]
pub fn directories(&self) -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = Vec::new();
for dir in &self.search_paths {
let dir = apply_sysroot(dir, self.sysroot.as_deref());
if !out.contains(&dir) {
out.push(dir);
}
}
out
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LibraryMatch {
pub object: String,
pub path: PathBuf,
pub flag: String,
pub kind: LibraryKind,
pub version: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AvailableVersion {
pub library: LibraryMatch,
pub default: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Hint {
MissingLibrary {
symbol: Vec<u8>,
library: LibraryMatch,
},
DroppedAsNeeded {
symbol: Vec<u8>,
library: LibraryMatch,
},
StaticOnly {
symbol: Vec<u8>,
library: LibraryMatch,
linked: PathBuf,
},
VersionMismatch {
symbol: Vec<u8>,
wanted: Option<String>,
available: Vec<AvailableVersion>,
},
NearMiss {
symbol: Vec<u8>,
near: NearMiss,
},
}
#[derive(Clone, Debug)]
pub struct Hinter {
scope: SearchScope,
linked: Vec<LinkedLibrary>,
}
impl Hinter {
#[must_use]
pub fn new(scope: SearchScope, linked: Vec<LinkedLibrary>) -> Self {
Self { scope, linked }
}
#[must_use]
pub fn hints(&self, undefined: &[Undefined<'_>], defined: &[&[u8]]) -> Vec<Vec<Hint>> {
let mut out = self.library_hints(undefined);
let names: Vec<&[u8]> = undefined.iter().map(|u| u.name).collect();
for ((hints, symbol), near) in out.iter_mut().zip(&names).zip(near_misses(&names, defined))
{
hints.extend(near.into_iter().map(|near| Hint::NearMiss {
symbol: symbol.to_vec(),
near,
}));
}
out
}
#[must_use]
pub fn library_hints(&self, undefined: &[Undefined<'_>]) -> Vec<Vec<Hint>> {
if undefined.is_empty() {
return Vec::new();
}
let names: Vec<&[u8]> = undefined.iter().map(|u| u.name).collect();
let index = LibraryIndex::build(&self.scope, &self.linked, &names);
undefined
.iter()
.map(|symbol| library_hints_for(&self.scope, &index, symbol))
.collect()
}
}
fn version_matches(definition: &Definition, wanted: Option<&[u8]>) -> bool {
match wanted {
Some(wanted) => definition.version.as_deref().map(str::as_bytes) == Some(wanted),
None => !definition.hidden,
}
}
fn library_match(
scope: &SearchScope,
index: &LibraryIndex,
definition: &Definition,
) -> Option<LibraryMatch> {
let object = index.objects().get(definition.object)?;
let kind = object.kind?;
let entry = index.best_entry(definition.object);
let (path, flag) = match entry {
Some(entry) => (entry.path.clone(), LibraryIndex::flag(scope, entry)),
None => (object.path.clone(), object.path.display().to_string()),
};
let display = match &definition.member {
Some(member) => format!("{}({member})", object.display),
None => object.display.clone(),
};
Some(LibraryMatch {
object: display,
path,
flag,
kind,
version: definition.version.clone(),
})
}
fn library_hints_for(
scope: &SearchScope,
index: &LibraryIndex,
symbol: &Undefined<'_>,
) -> Vec<Hint> {
let definitions = index.definitions(symbol.name);
let mut hints = Vec::new();
let mut seen_objects: Vec<usize> = Vec::new();
let mut candidates: Vec<(LibraryMatch, usize, Option<&LinkedLibrary>)> = Vec::new();
for definition in definitions {
if !version_matches(definition, symbol.version) || seen_objects.contains(&definition.object)
{
continue;
}
seen_objects.push(definition.object);
let Some(library) = library_match(scope, index, definition) else {
continue;
};
let dir = index
.best_entry(definition.object)
.map_or(usize::MAX, |entry| entry.dir);
candidates.push((library, dir, index.linked(definition.object)));
}
candidates.sort_by(|a, b| {
(a.1, a.0.kind, &a.0.path, &a.0.object).cmp(&(b.1, b.0.kind, &b.0.path, &b.0.object))
});
let mut stems: Vec<String> = Vec::new();
candidates.retain(|(library, _, _)| {
let stem = library_stem(&library.path);
if stems.contains(&stem) {
false
} else {
stems.push(stem);
true
}
});
for (library, _, linked) in candidates {
let symbol = symbol.name.to_vec();
match linked {
Some(linked) if linked.dropped_as_needed => {
hints.push(Hint::DroppedAsNeeded { symbol, library });
}
Some(_) => {}
None => {
let archive = (library.kind == LibraryKind::Shared)
.then(|| library::library_name(&library.path, ".so"))
.flatten()
.and_then(|name| index.linked_static(&name));
match archive {
Some(archive) => hints.push(Hint::StaticOnly {
symbol,
library,
linked: archive.to_path_buf(),
}),
None => hints.push(Hint::MissingLibrary { symbol, library }),
}
}
}
}
hints.truncate(MAX_LIBRARIES);
if seen_objects.is_empty() && !definitions.is_empty() {
let mut available: Vec<AvailableVersion> = Vec::new();
for definition in definitions {
let Some(library) = library_match(scope, index, definition) else {
continue;
};
let version = AvailableVersion {
library,
default: !definition.hidden,
};
if !available.contains(&version) {
available.push(version);
}
}
if available.iter().any(|v| v.library.version.is_some()) {
available.retain(|v| v.library.version.is_some());
}
available.sort_by(|a, b| {
a.library.path.cmp(&b.library.path).then_with(|| {
natural_cmp(
a.library.version.as_deref().unwrap_or_default(),
b.library.version.as_deref().unwrap_or_default(),
)
})
});
if !available.is_empty() {
hints.push(Hint::VersionMismatch {
symbol: symbol.name.to_vec(),
wanted: symbol
.version
.map(|v| String::from_utf8_lossy(v).into_owned()),
available,
});
}
}
hints
}
fn library_stem(path: &Path) -> String {
let file = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let name = file.strip_prefix("lib").unwrap_or(&file);
let end = [name.find(".so"), name.strip_suffix(".a").map(str::len)]
.into_iter()
.flatten()
.min()
.unwrap_or(name.len());
name.get(..end).unwrap_or(name).to_string()
}
fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering {
fn runs(text: &str) -> Vec<(bool, &str)> {
let mut out = Vec::new();
let mut start = 0;
let bytes = text.as_bytes();
for i in 1..=bytes.len() {
let boundary = i == bytes.len()
|| bytes.get(i).map(u8::is_ascii_digit)
!= bytes.get(i.saturating_sub(1)).map(u8::is_ascii_digit);
if boundary {
let run = text.get(start..i).unwrap_or_default();
out.push((run.as_bytes().first().is_some_and(u8::is_ascii_digit), run));
start = i;
}
}
out
}
let (ra, rb) = (runs(a), runs(b));
for ((digits_a, x), (digits_b, y)) in ra.iter().zip(&rb) {
let ordering = if *digits_a && *digits_b {
let (x, y) = (x.trim_start_matches('0'), y.trim_start_matches('0'));
x.len().cmp(&y.len()).then_with(|| x.cmp(y))
} else {
x.cmp(y)
};
if ordering.is_ne() {
return ordering;
}
}
ra.len().cmp(&rb.len())
}
#[must_use]
pub fn display_symbol(name: &[u8], demangle: bool) -> Cow<'_, str> {
if demangle {
crate::demangle::demangle(name)
} else {
String::from_utf8_lossy(name)
}
}
#[must_use]
pub fn render(hint: &Hint, demangle: bool) -> String {
let show = |name: &[u8]| display_symbol(name, demangle).into_owned();
let place = |library: &LibraryMatch| {
let path = library.path.display().to_string();
if library.object == path
|| Path::new(&path)
.file_name()
.is_some_and(|n| n.to_string_lossy() == library.object)
{
path
} else {
format!("{} ({path})", library.object)
}
};
match hint {
Hint::MissingLibrary { symbol, library } => format!(
"'{}' is defined in {}; did you forget {}?",
show(symbol),
place(library),
library.flag
),
Hint::DroppedAsNeeded { symbol, library } => format!(
"'{}' is defined in {}, which --as-needed dropped because nothing needed it yet; put {} after the files that use it",
show(symbol),
place(library),
library.flag
),
Hint::StaticOnly {
symbol,
library,
linked,
} => format!(
"'{}' is defined in {}, but -Bstatic linked {} instead, which does not define it",
show(symbol),
place(library),
linked.display()
),
Hint::VersionMismatch {
symbol,
wanted,
available,
} => {
let mut by_library: Vec<(String, Vec<String>)> = Vec::new();
for version in available {
let name = match &version.library.version {
Some(v) if version.default => format!("{v} (default)"),
Some(v) => v.clone(),
None => "unversioned".to_string(),
};
let key = place(&version.library);
match by_library.iter_mut().find(|(k, _)| *k == key) {
Some((_, names)) => names.push(name),
None => by_library.push((key, vec![name])),
}
}
let list = by_library
.iter()
.map(|(library, versions)| format!("{} in {library}", versions.join(", ")))
.collect::<Vec<_>>()
.join("; ");
match wanted {
Some(wanted) => format!(
"'{}' is not defined at version {wanted}; available: {list}",
show(symbol)
),
None => format!(
"'{}' has no default version to bind to; available: {list}",
show(symbol)
),
}
}
Hint::NearMiss { symbol, near } => {
let candidate = show(&near.candidate);
match near.kind {
NearMissKind::CppDefinition => {
format!("did you mean to declare {candidate} as extern \"C\"?")
}
NearMissKind::CDefinition => format!(
"did you mean: extern \"C\" {candidate}? ('{}' has C++ linkage)",
show(symbol)
),
NearMissKind::Underscore => {
format!("did you mean: {candidate}? (leading underscore)")
}
NearMissKind::Parameters => {
format!("did you mean: {candidate}? (parameter types differ)")
}
NearMissKind::Qualifiers => {
format!("did you mean: {candidate}? (qualifiers differ)")
}
NearMissKind::Scope => format!("did you mean: {candidate}? (different scope)"),
NearMissKind::Spelling(_) => format!("did you mean: {candidate}?"),
}
}
}
}
#[must_use]
pub fn attach(mut diagnostic: Diagnostic, hints: &[Hint], demangle: bool) -> Diagnostic {
for hint in hints {
diagnostic = diagnostic.note(render(hint, demangle));
}
diagnostic
}
#[cfg(test)]
mod tests;