From 57c96df288dfc7046280c49d3f4445feac335ce0 Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 4 Jan 2024 20:56:38 +0100 Subject: [PATCH] Explain ld errors (#773) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) ``` --- crates/puffin-build/src/lib.rs | 105 ++++++++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 16 deletions(-) diff --git a/crates/puffin-build/src/lib.rs b/crates/puffin-build/src/lib.rs index 372d4ed13..bc8fc1e60 100644 --- a/crates/puffin-build/src/lib.rs +++ b/crates/puffin-build/src/lib.rs @@ -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 = Lazy::new(|| { Regex::new( - r".*\.(c|c..|h|h..):\d+:\d+: fatal error: (?
.*\.(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 = 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)" + ); + } }