Skip to content

Commit 9a76c29

Browse files
committed
Initial implementation of abi-check.py
1 parent eace355 commit 9a76c29

File tree

4 files changed

+132
-18
lines changed

4 files changed

+132
-18
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -74,31 +74,17 @@ jobs:
7474
runs-on: ubuntu-latest
7575
steps:
7676
- uses: actions/checkout@v3
77+
with:
78+
fetch-depth: 0
7779
- name: Install dependencies
7880
run: |
7981
sudo apt update
8082
sudo apt install liblzo2-dev libssl-dev gnutls-dev libgcrypt-dev abi-dumper abi-compliance-checker universal-ctags
81-
- name: Build libraries
82-
env:
83-
CFLAGS: "-gdwarf-4 -Og"
84-
run: |
85-
mkdir build
86-
cd build
87-
cmake ..
88-
cmake --build . --target vncclient
89-
cmake --build . --target vncserver
9083
- name: Check ABI
91-
run: |
92-
mkdir abi-check
93-
cd abi-check
94-
abi-dumper -lver $GITHUB_REF_NAME ../build/libvncclient.so -o client-abi-new.dump -public-headers ../rfb
95-
abi-dumper -lver $GITHUB_REF_NAME ../build/libvncserver.so -o server-abi-new.dump -public-headers ../rfb
96-
abi-compliance-checker -l LibVNCClient -old ../utils/abi/client-abi-v1.dump -new client-abi-new.dump -report-path client-report.html
97-
abi-compliance-checker -l LibVNCServer -old ../utils/abi/server-abi-v1.dump -new server-abi-new.dump -report-path server-report.html
98-
84+
run: ./test/abi/abi-check.py
9985
- uses: actions/upload-artifact@v3
10086
if: always()
10187
with:
10288
name: abi-check-result
103-
path: abi-check
89+
path: test/abi/abi-check-result
10490

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ test/cargstest
4040
test/copyrecttest
4141
test/encodingstest
4242
test/wstest
43+
test/abi/abi-check-result
4344
/test/tjbench
4445
/test/tjunittest
4546
vncterm/LinuxVNC

test/abi/abi-check.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/python3
2+
3+
import os
4+
import sys
5+
import argparse
6+
import tempfile
7+
import subprocess
8+
from pathlib import Path
9+
10+
11+
ORIGINAL_DIR = Path(os.getcwd())
12+
SCRIPT_DIR = Path(__file__).resolve().parent
13+
REPO_DIR = SCRIPT_DIR.parent.parent # Assuming this script is in 'test/abi'
14+
WORK_DIR = tempfile.TemporaryDirectory(prefix='libvnc-abi-check')
15+
16+
DEFAULT_REV_FILE = Path(SCRIPT_DIR, 'published-abi-revision') # May not exist
17+
OUTPUT_DIR = Path(SCRIPT_DIR, 'abi-check-result') # ABI dumps & compliance reports are generated here
18+
19+
LIB_CLIENT = 'vncclient'
20+
LIB_SERVER = 'vncserver'
21+
LABEL_OLD = 'old'
22+
LABEL_NEW = 'new'
23+
24+
25+
def run_cmd(cmd: str, check=True):
26+
return subprocess.run(cmd, shell=True, check=check)
27+
28+
29+
def read_cmd_output(cmd: str):
30+
return subprocess.run(cmd, shell=True, check=True, stdout=subprocess.PIPE).stdout
31+
32+
33+
def create_dump_file_path(library: str, label: str):
34+
return str(Path(OUTPUT_DIR, f"{library}-{label}.dump"))
35+
36+
# Builds given library (vncclient/vncserver), and stores it's ABI dump in output directory.
37+
# Assumes we are in build directory.
38+
39+
40+
def dump_library_abi(library: str, label: str):
41+
dump_file = create_dump_file_path(library, label)
42+
run_cmd(f"cmake --build . --target {library}")
43+
run_cmd(f"abi-dumper -lver {label} lib{library}.so -o {dump_file} -public-headers ../rfb")
44+
45+
# Dumps ABIs for given revision
46+
47+
48+
def dump_abi(rev: str, label: str):
49+
tree_dir = Path(WORK_DIR.name, label)
50+
build_dir = Path(tree_dir, "build")
51+
run_cmd(f"git -C {str(REPO_DIR)} worktree add {tree_dir} {rev}")
52+
os.mkdir(build_dir)
53+
os.chdir(build_dir)
54+
run_cmd("env CFLAGS='-gdwarf-4 -Og' cmake ..")
55+
dump_library_abi(LIB_CLIENT, label)
56+
dump_library_abi(LIB_SERVER, label)
57+
58+
59+
def compare_library_abi(library: str):
60+
old_abi = create_dump_file_path(library, LABEL_OLD)
61+
new_abi = create_dump_file_path(library, LABEL_NEW)
62+
report = str(Path(OUTPUT_DIR, f"{library}-report.html"))
63+
r = run_cmd(f"abi-compliance-checker -l {library} -old {old_abi} -new {new_abi} -report-path {report}", False)
64+
if r.returncode != 0:
65+
print(f"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
66+
print(f"~ ERROR: ABI break detected in {library}")
67+
print(f"~ Please check the report at file://{report}")
68+
print(f"~ On GitHub Actions, this report is also available in workflow artifacts")
69+
print(f"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
70+
sys.exit(1)
71+
72+
73+
def compare(old_rev: str, new_rev: str):
74+
dump_abi(old_rev, LABEL_OLD)
75+
dump_abi(new_rev, LABEL_NEW)
76+
compare_library_abi(LIB_CLIENT)
77+
compare_library_abi(LIB_SERVER)
78+
79+
80+
def update():
81+
head = read_cmd_output(f"git -C {str(REPO_DIR)} rev-list HEAD --max-count=1")
82+
DEFAULT_REV_FILE.write_bytes(head)
83+
84+
85+
def parse_args():
86+
rf_name = DEFAULT_REV_FILE.name
87+
parser = argparse.ArgumentParser(description="Check ABI compatibility between two Git revisions")
88+
parser.add_argument('-o', dest='old', help=f"Old revision; defaults to reading from '{rf_name}'")
89+
parser.add_argument('-n', dest='new', help="New revision; defaults to 'HEAD'")
90+
parser.add_argument('-u', dest='update', help=f"Update '{rf_name}' file with current 'HEAD'", action='store_true')
91+
args = parser.parse_args()
92+
93+
if args.update:
94+
return args
95+
96+
if args.old == None:
97+
if DEFAULT_REV_FILE.exists():
98+
with open(DEFAULT_REV_FILE) as f:
99+
args.old = f.readline()
100+
else:
101+
print(f"ERROR: Cannot detect old revision automatically, '{str(DEFAULT_REV_FILE)}' is missing")
102+
sys.exit(1)
103+
104+
if args.new == None:
105+
args.new = read_cmd_output(f"git -C {str(REPO_DIR)} rev-list HEAD --max-count=1").decode().strip()
106+
if args.new == None:
107+
print("ERROR: Cannot detect new revision automatically from git repo")
108+
sys.exit(1)
109+
110+
return args
111+
112+
113+
def main():
114+
try:
115+
args = parse_args()
116+
if args.update:
117+
update()
118+
else:
119+
compare(args.old, args.new)
120+
finally:
121+
os.chdir(ORIGINAL_DIR) # Restore
122+
WORK_DIR.cleanup()
123+
run_cmd(f"git -C {str(REPO_DIR)} worktree prune", check=False)
124+
125+
126+
main()

test/abi/published-abi-revision

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
10e9eb75f73e973725dc75c373de5d89807af028

0 commit comments

Comments
 (0)