Skip to content

Commit b51dc00

Browse files
authored
Test that linux binary depends on glibc 2.18 and no other dynamic symbols (clangd#372)
This regressed twice over the last two months (new floating point function versions, and accidental dynamic linking against zlib). We also want to avoid regressions when merging remote index. The test is able to do a little bit more than we use in the automated build (the --sym flag is unused, as is unversioned --lib=GLIBC) but they're pretty useful when experimenting with how to fix things! We run the test right at the end, because if it fails we want to be able to download the binary artifact and inspect it. Unfortunately by the nature of the test we can only run it when we produce a build, so currently weekly.
1 parent 2de2ec4 commit b51dc00

File tree

3 files changed

+94
-1
lines changed

3 files changed

+94
-1
lines changed

.github/workflows/autobuild.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ jobs:
142142
asset_name: clangd-${{ matrix.config.name }}-${{ github.event.release.tag_name }}.zip
143143
asset_path: clangd.zip
144144
asset_content_type: application/zip
145+
- name: Check binary compatibility
146+
if: matrix.config.name == 'linux'
147+
run: .github/worflows/lib_compat_test.py --lib=GLIBC_2.18 "$CLANGD_DIR/bin/clangd"
145148
# Create the release, and upload the artifacts to it.
146149
finalize:
147150
runs-on: ubuntu-latest

.github/workflows/lib_compat_test.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env python3
2+
3+
# Verifies a binary uses only dynamic symbols from whitelisted library versions.
4+
# Prints the disallowed symbols and their versions on failure.
5+
# Usage: lib_compat_test.py bin/clangd --lib=GLIBC_2.18
6+
7+
import argparse
8+
import subprocess
9+
import sys
10+
11+
parser = argparse.ArgumentParser()
12+
parser.add_argument("binary")
13+
parser.add_argument(
14+
"--lib",
15+
action="append",
16+
default=[],
17+
help="Whitelist a library, e.g. GLIBC_2.18 or GLIBC",
18+
)
19+
parser.add_argument(
20+
"--sym", action="append", default=[], help="Whitelist a symbol, e.g. crc32"
21+
)
22+
args = parser.parse_args()
23+
24+
# Parses GLIBC_2.3 into ("GLIBC", [2,3])
25+
# Parses GLIBC into ("GLIBC", None)
26+
def parse_version(version):
27+
parts = version.rsplit("_", 1)
28+
if len(parts) == 1:
29+
return (version, None)
30+
try:
31+
return (parts[0], [int(p) for p in parts[1].split(".")])
32+
except ValueError:
33+
return (version, None)
34+
35+
36+
lib_versions = dict([parse_version(v) for v in args.lib])
37+
38+
# Determines whether all symbols with version 'lib' are acceptable.
39+
# A versioned library is name_x.y.z by convention.
40+
def accept_lib(lib):
41+
(lib, ver) = parse_version(lib)
42+
if not lib in lib_versions: # Non-whitelisted library.
43+
return False
44+
if lib_versions[lib] is None: # Unrestricted version
45+
return True
46+
if ver is None: # Lib has non-numeric version, library restricts version.
47+
return False
48+
return ver <= lib_versions[lib]
49+
50+
51+
# Determines whether an optionally-versioned symbol is acceptable.
52+
# A versioned symbol is symbol@version as output by nm.
53+
def accept_symbol(sym):
54+
if sym in args.sym:
55+
return True
56+
split = sym.split("@", 1)
57+
return (split[0] in args.sym) or (len(split) == 2 and accept_lib(split[1]))
58+
59+
60+
# Run nm to find the undefined symbols, and check whether each is acceptable.
61+
nm = subprocess.run(
62+
["nm", "-uD", "--with-symbol-version", args.binary],
63+
stdout=subprocess.PIPE,
64+
text=True,
65+
)
66+
nm.check_returncode()
67+
status = 0
68+
for line in nm.stdout.splitlines():
69+
# line = " U foo@GLIBC_2.3"
70+
parts = line.split()
71+
if len(parts) != 2:
72+
print("Unparseable nm output: ", line, file=sys.stderr)
73+
status = 2
74+
continue
75+
if parts[0] == "w": # Weak-undefined symbol, not actually required.
76+
continue
77+
if not accept_symbol(parts[1]):
78+
print(parts[1])
79+
status = 1
80+
if status == 1:
81+
print(
82+
"Binary depends on disallowed symbols above. Use some combination of:\n"
83+
" - relax the whitelist by adding --lib and --sym flags to this test\n"
84+
" - force older symbol versions by updating lib_compat.h\n"
85+
" - avoid dynamic dependencies by changing CMake configuration\n"
86+
" - remove bad dependencies from the code",
87+
file=sys.stderr,
88+
)
89+
sys.exit(status)

releases.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ is being able to cut releases easily whenever we want.
88
The releases are just a zip archive containing the `clangd` binary, and the
99
clang builtin headers. They should be runnable immediately after extracting the
1010
archive. The linux binary has `libstdc++` and other dependencies statically
11-
linked for maximum portability.
11+
linked for maximum portability, and requires glibc 2.18 (the first version with
12+
`thread_local` support).
1213

1314
## Creating a release manually
1415

0 commit comments

Comments
 (0)