Explain ld errors (#773)

One of the most common ways source dists fail to build (on linux) is
when the linker fails because the shared library of a native dependency
is not installed. These errors are hard to understand when you're not a
c programmer:

```
       In file included from /usr/include/python3.10/unicodeobject.h:1046,
                        from /usr/include/python3.10/Python.h:83,
                        from Modules/3.x/readline.c:8:
       Modules/3.x/readline.c: In function ‘on_completion’:
       /usr/include/python3.10/cpython/unicodeobject.h:744:29: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
         744 | #define _PyUnicode_AsString PyUnicode_AsUTF8
             |                             ^~~~~~~~~~~~~~~~
       Modules/3.x/readline.c:842:23: note: in expansion of macro ‘_PyUnicode_AsString’
         842 |             char *s = _PyUnicode_AsString(r);
             |                       ^~~~~~~~~~~~~~~~~~~
       Modules/3.x/readline.c: In function ‘readline_until_enter_or_signal’:
       Modules/3.x/readline.c:1044:9: warning: ‘sigrelse’ is deprecated: Use the sigprocmask function instead [-Wdeprecated-declarations]
        1044 |         sigrelse(SIGINT);
             |         ^~~~~~~~
       In file included from Modules/3.x/readline.c:10:
       /usr/include/signal.h:359:12: note: declared here
         359 | extern int sigrelse (int __sig) __THROW
             |            ^~~~~~~~
       Modules/3.x/readline.c: In function ‘PyInit_readline’:
       Modules/3.x/readline.c:1179:34: warning: assignment to ‘char * (*)(FILE *, FILE *, const char *)’ from incompatible pointer type ‘char * (*)(FILE *, FILE *, char *)’ [-Wincompatible-pointer-types]
        1179 |     PyOS_ReadlineFunctionPointer = call_readline;
             |                                  ^
       In file included from /usr/include/string.h:535,
                        from /usr/include/python3.10/Python.h:30,
                        from Modules/3.x/readline.c:8:
       In function ‘strncpy’,
           inlined from ‘call_readline’ at Modules/3.x/readline.c:1124:9:
       /usr/include/x86_64-linux-gnu/bits/string_fortified.h:95:10: warning: ‘__builtin_strncpy’ output truncated before terminating nul copying as many bytes from a string as its length [-Wstringop-truncation]
          95 |   return __builtin___strncpy_chk (__dest, __src, __len,
             |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          96 |                                   __glibc_objsize (__dest));
             |                                   ~~~~~~~~~~~~~~~~~~~~~~~~~
       Modules/3.x/readline.c: In function ‘call_readline’:
       Modules/3.x/readline.c:1099:9: note: length computed here
        1099 |     n = strlen(p);
             |         ^~~~~~~~~
       /usr/bin/ld: cannot find -lncurses: No such file or directory
       collect2: error: ld returned 1 exit status
       error: command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1
       ---
```

We parse these errors out, tell the user about the missing shared
library and even the most likely debian/ubuntu package name:

```
This error likely indicates that you need to install the library that provides a shared library for ncurses for pygraphviz-1.11 (e.g. libncurses-dev)
```
This commit is contained in:
konsti 2024-01-04 20:56:38 +01:00 committed by GitHub
parent 8ac6f9a198
commit 57c96df288
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -34,9 +34,13 @@ use puffin_traits::{BuildContext, BuildKind, SourceBuildTrait};
/// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory`
static MISSING_HEADER_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r".*\.(c|c..|h|h..):\d+:\d+: fatal error: (?<header>.*\.(h|h..)): No such file or directory"
r".*\.(?:c|c..|h|h..):\d+:\d+: fatal error: (.*\.(?:h|h..)): No such file or directory",
)
.unwrap()
.unwrap()
});
/// e.g. `/usr/bin/ld: cannot find -lncurses: No such file or directory`
static LD_NOT_FOUND_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"/usr/bin/ld: cannot find -l([a-zA-Z10-9]+): No such file or directory").unwrap()
});
#[derive(Error, Debug)]
@ -76,9 +80,15 @@ pub enum Error {
},
}
#[derive(Debug)]
pub enum MissingLibrary {
Header(String),
Linker(String),
}
#[derive(Debug, Error)]
pub struct MissingHeaderCause {
header: String,
missing_library: MissingLibrary,
// I've picked this over the better readable package name to make clear that you need to
// look for the build dependencies of that version or git commit respectively
package_id: String,
@ -86,11 +96,23 @@ pub struct MissingHeaderCause {
impl Display for MissingHeaderCause {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"This error likely indicates that you need to install a library that provides \"{}\" for {}",
self.header, self.package_id
)
match &self.missing_library {
MissingLibrary::Header(header) => {
write!(
f,
"This error likely indicates that you need to install a library that provides \"{}\" for {}",
header, self.package_id
)
}
MissingLibrary::Linker(library) => {
write!(
f,
"This error likely indicates that you need to install the library that provides a shared library \
for {library} for {package_id} (e.g. lib{library}-dev)",
library=library, package_id=self.package_id
)
}
}
}
}
@ -103,19 +125,28 @@ impl Error {
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
// In the cases i've seen it was the 5th last line (see test case), 10 seems like a
// reasonable cutoff
if let Some(header) =
stderr.lines().rev().take(10).find_map(|line| {
Some(MISSING_HEADER_RE.captures(line.trim())?["header"].to_string())
})
{
// In the cases i've seen it was the 5th and 3rd last line (see test case), 10 seems like a reasonable cutoff
let missing_library = stderr.lines().rev().take(10).find_map(|line| {
if let Some((_, [header])) =
MISSING_HEADER_RE.captures(line.trim()).map(|c| c.extract())
{
Some(MissingLibrary::Header(header.to_string()))
} else if let Some((_, [library])) =
LD_NOT_FOUND_RE.captures(line.trim()).map(|c| c.extract())
{
Some(MissingLibrary::Linker(library.to_string()))
} else {
None
}
});
if let Some(missing_library) = missing_library {
return Self::MissingHeader {
message,
stdout,
stderr,
missing_header_cause: MissingHeaderCause {
header,
missing_library,
package_id: package_id.into(),
},
};
@ -740,4 +771,46 @@ mod test {
@r###"This error likely indicates that you need to install a library that provides "graphviz/cgraph.h" for pygraphviz-1.11"###
);
}
#[test]
fn missing_linker_library() {
let output = Output {
status: ExitStatus::default(), // This is wrong but `from_raw` is platform-gated.
stdout: Vec::new(),
stderr: indoc!(
r"
1099 | n = strlen(p);
| ^~~~~~~~~
/usr/bin/ld: cannot find -lncurses: No such file or directory
collect2: error: ld returned 1 exit status
error: command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1
"
)
.as_bytes()
.to_vec(),
};
let err = Error::from_command_output(
"Failed building wheel through setup.py".to_string(),
&output,
"pygraphviz-1.11",
);
assert!(matches!(err, Error::MissingHeader { .. }));
insta::assert_display_snapshot!(err, @r###"
Failed building wheel through setup.py:
--- stdout:
--- stderr:
1099 | n = strlen(p);
| ^~~~~~~~~
/usr/bin/ld: cannot find -lncurses: No such file or directory
collect2: error: ld returned 1 exit status
error: command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1
---
"###);
insta::assert_display_snapshot!(
std::error::Error::source(&err).unwrap(),
@"This error likely indicates that you need to install the library that provides a shared library for ncurses for pygraphviz-1.11 (e.g. libncurses-dev)"
);
}
}