1#!/usr/bin/perl
2
3# verify-exports.pl
4# Check exports in a library vs. declarations in header files.
5# usage: verify-exports.pl /path/to/dylib /glob/path/to/headers decl-prefix [-arch <arch>] [/path/to/project~dst]
6# example: verify-exports.pl /usr/lib/libobjc.A.dylib '/usr/{local/,}include/objc/*' OBJC_EXPORT -arch x86_64 /tmp/objc-test.roots/objc-test~dst
7
8# requirements:
9# - every export must have an @interface or specially-marked declaration
10# - every @interface or specially-marked declaration must have an availability macro
11# - no C++ exports allowed
12
13use strict;
14use File::Basename;
15use File::Glob ':glob';
16
17my $bad = 0;
18
19$0 = basename($0, ".pl");
20my $usage = "/path/to/dylib /glob/path/to/headers decl-prefix [-arch <arch>] [-sdk sdkname] [/path/to/project~dst]";
21
22my $lib_arg = shift || die "$usage";
23die "$usage" unless ($lib_arg =~ /^\//);
24my $headers_arg = shift || die "$usage";
25my $export_arg = shift || die "$usage";
26
27my $arch = "x86_64";
28if ($ARGV[0] eq "-arch") {
29    shift;
30    $arch = shift || die "$0: -arch requires an architecture";
31}
32my $sdk = "system";
33if ($ARGV[0] eq "-sdk") {
34    shift;
35    $sdk = shift || die "$0: -sdk requires an SDK name";
36}
37
38my $root = shift || "";
39
40
41# Collect symbols from dylib.
42my $lib_path = "$root$lib_arg";
43die "$0: file not found: $lib_path\n" unless -e $lib_path;
44
45my %symbols;
46my @symbollines = `nm -arch $arch '$lib_path'`;
47die "$0: nm failed: (arch $arch) $lib_path\n" if ($?);
48for my $line (@symbollines) {
49    chomp $line;
50    (my $type, my $name) = ($line =~ /^[[:xdigit:]]*\s+(.) (.*)$/);
51    if ($type =~ /^[A-TV-Z]$/) {
52        $symbols{$name} = 1;
53    } else {
54        # undefined (U) or non-external - ignore
55    }
56}
57
58# Complain about C++ exports
59for my $symbol (keys %symbols) {
60    if ($symbol =~ /^__Z/) {
61        print "BAD: C++ export '$symbol'\n"; $bad++;
62    }
63}
64
65
66# Translate arch to unifdef(1) parameters: archnames, __LP64__, __OBJC2__
67my @archnames = ("x86_64", "i386", "arm", "armv6", "armv7");
68my %archOBJC1   = (i386 => 1);
69my %archLP64    = (x86_64 => 1);
70my @archparams;
71
72my $OBJC1 = ($archOBJC1{$arch} && $sdk !~ /^iphonesimulator/);
73
74if ($OBJC1) {
75    push @archparams, "-U__OBJC2__";
76} else {
77    push @archparams, "-D__OBJC2__=1";
78}
79
80if ($archLP64{$arch}) { push @archparams, "-D__LP64__=1"; }
81else { push @archparams, "-U__LP64__"; }
82
83for my $archname (@archnames) {
84    if ($archname eq $arch) {
85        push @archparams, "-D__${archname}__=1";
86        push @archparams, "-D__$archname=1";
87    } else {
88        push @archparams, "-U__${archname}__";
89        push @archparams, "-U__$archname";
90    }
91}
92
93# TargetConditionals.h
94# fixme iphone and simulator
95push @archparams, "-DTARGET_OS_WIN32=0";
96push @archparams, "-DTARGET_OS_EMBEDDED=0";
97push @archparams, "-DTARGET_OS_IPHONE=0";
98push @archparams, "-DTARGET_OS_MAC=1";
99
100# Gather declarations from header files
101# A C declaration starts with $export_arg and ends with ';'
102# A class declaration is @interface plus the line before it.
103my $unifdef_cmd = "/usr/bin/unifdef " . join(" ", @archparams);
104my @cdecls;
105my @classdecls;
106for my $header_path(bsd_glob("$root$headers_arg",GLOB_BRACE)) {
107    my $header;
108    # feed through unifdef(1) first to strip decls from other archs
109    # fixme strip other SDKs as well
110    open($header, "$unifdef_cmd < '$header_path' |");
111    my $header_contents = join("", <$header>);
112
113    # C decls
114    push @cdecls, ($header_contents =~ /^\s*$export_arg\s+([^;]*)/msg);
115
116    # ObjC classes, but not categories.
117    # fixme ivars
118    push @classdecls, ($header_contents =~ /^([^\n]*\n\s*\@interface\s+[^(\n]+\n)/mg);
119}
120
121# Find name and availability of C declarations
122my %declarations;
123for my $cdecl (@cdecls) {
124    $cdecl =~ s/\n/ /mg;  # strip newlines
125
126    # Pull availability macro off the end:
127    # __OSX_AVAILABLE_*(*)
128    # AVAILABLE_MAC_OS_X_VERSION_*
129    # OBJC2_UNAVAILABLE
130    # OBJC_HASH_AVAILABILITY
131    # OBJC_MAP_AVAILABILITY
132    # UNAVAILABLE_ATTRIBUTE
133    # (DEPRECATED_ATTRIBUTE is not good enough. Be specific.)
134    my $avail = undef;
135    my $cdecl2;
136    ($cdecl2, $avail) = ($cdecl =~ /^(.*)\s+(__OSX_AVAILABLE_\w+\([a-zA-Z0-9_, ]+\))\s*$/) if (!defined $avail);
137    ($cdecl2, $avail) = ($cdecl =~ /^(.*)\s+(AVAILABLE_MAC_OS_X_VERSION_\w+)\s*$/) if (!defined $avail);
138    ($cdecl2, $avail) = ($cdecl =~ /^(.*)\s+(OBJC2_UNAVAILABLE)\s*$/) if (!defined $avail);
139    ($cdecl2, $avail) = ($cdecl =~ /^(.*)\s+(OBJC_GC_UNAVAILABLE)\s*$/) if (!defined $avail);
140    ($cdecl2, $avail) = ($cdecl =~ /^(.*)\s+(OBJC_ARC_UNAVAILABLE)\s*$/) if (!defined $avail);
141    ($cdecl2, $avail) = ($cdecl =~ /^(.*)\s+(OBJC_HASH_AVAILABILITY)\s*$/) if (!defined $avail);
142    ($cdecl2, $avail) = ($cdecl =~ /^(.*)\s+(OBJC_MAP_AVAILABILITY)\s*$/) if (!defined $avail);
143    ($cdecl2, $avail) = ($cdecl =~ /^(.*)\s+(UNAVAILABLE_ATTRIBUTE)\s*$/) if (!defined $avail);
144    # ($cdecl2, $avail) = ($cdecl =~ /^(.*)\s+(DEPRECATED_\w+)\s*$/) if (!defined $avail);
145    $cdecl2 = $cdecl if (!defined $cdecl2);
146
147    # Extract declaration name (assumes availability macro is already gone):
148    # `(*xxx)` (function pointer)
149    # `xxx(`   (function)
150    # `xxx`$` or `xxx[nnn]$` (variable or array variable)
151    my $name = undef;
152    ($name) = ($cdecl2 =~ /^[^(]*\(\s*\*\s*(\w+)\s*\)/) if (!defined $name);
153    ($name) = ($cdecl2 =~ /(\w+)\s*\(/) if (!defined $name);
154    ($name) = ($cdecl2 =~ /(\w+)\s*(?:\[\d*\]\s*)*$/) if (!defined $name);
155
156    if (!defined $name) {
157        print "BAD: unintellible declaration:\n    $cdecl\n"; $bad++;
158    } elsif (!defined $avail) {
159        print "BAD: no availability on declaration of '$name':\n    $cdecl\n"; $bad++;
160    }
161
162    if ($avail eq "UNAVAILABLE_ATTRIBUTE")
163    {
164        $declarations{$name} = "unavailable";
165    } elsif ($avail eq "OBJC2_UNAVAILABLE"  &&  ! $OBJC1) {
166        # fixme OBJC2_UNAVAILABLE may or may not have an exported symbol
167        # $declarations{$name} = "unavailable";
168    } else {
169        $declarations{"_$name"} = "available";
170    }
171}
172
173# Find name and availability of Objective-C classes
174for my $classdecl (@classdecls) {
175    $classdecl =~ s/\n/ /mg;  # strip newlines
176
177    # Pull availability macro off the front:
178    # __OSX_AVAILABLE_*(*)
179    # AVAILABLE_MAC_OS_X_VERSION_*
180    # OBJC2_UNAVAILABLE
181    # OBJC_HASH_AVAILABILITY
182    # OBJC_MAP_AVAILABILITY
183    # UNAVAILABLE_ATTRIBUTE
184    # (DEPRECATED_ATTRIBUTE is not good enough. Be specific.)
185    my $avail = undef;
186    my $classdecl2;
187    ($avail, $classdecl2) = ($classdecl =~ /^\s*(__OSX_AVAILABLE_\w+\([a-zA-Z0-9_, ]+\))\s*(.*)$/) if (!defined $avail);
188    ($avail, $classdecl2) = ($classdecl =~ /^\s*(AVAILABLE_MAC_OS_X_VERSION_\w+)\s*(.*)$/) if (!defined $avail);
189    ($avail, $classdecl2) = ($classdecl =~ /^\s*(OBJC2_UNAVAILABLE)\s*(.*)$/) if (!defined $avail);
190    ($avail, $classdecl2) = ($classdecl =~ /^\s*(OBJC_HASH_AVAILABILITY)\s*(.*)$/) if (!defined $avail);
191    ($avail, $classdecl2) = ($classdecl =~ /^\s*(OBJC_MAP_AVAILABILITY)\s*(.*)$/) if (!defined $avail);
192    ($avail, $classdecl2) = ($classdecl =~ /^\s*(UNAVAILABLE_ATTRIBUTE)\s*(.*)$/) if (!defined $avail);
193    # ($avail, $classdecl2) = ($classdecl =~ /^\s*(DEPRECATED_\w+)\s*(.*)$/) if (!defined $avail);
194    $classdecl2 = $classdecl if (!defined $classdecl2);
195
196    # Extract class name.
197    my $name = undef;
198    ($name) = ($classdecl2 =~ /\@interface\s+(\w+)/);
199
200    if (!defined $name) {
201        print "BAD: unintellible declaration:\n    $classdecl\n"; $bad++;
202    } elsif (!defined $avail) {
203        print "BAD: no availability on declaration of '$name':\n    $classdecl\n"; $bad++;
204    }
205
206    my $availability;
207    if ($avail eq "UNAVAILABLE_ATTRIBUTE") {
208	$availability = "unavailable";
209    } elsif ($avail eq "OBJC2_UNAVAILABLE"  &&  ! $OBJC1) {
210        # fixme OBJC2_UNAVAILABLE may or may not have an exported symbol
211        # $declarations{$name} = "unavailable";
212	$availability = undef;
213    } else {
214	$availability = "available";
215    }
216
217    if (! $OBJC1) {
218        $declarations{"_OBJC_CLASS_\$_$name"} = $availability;
219        $declarations{"_OBJC_METACLASS_\$_$name"} = $availability;
220        # fixme ivars
221        $declarations{"_OBJC_IVAR_\$_$name.isa"} = $availability if ($name eq "Object");
222    } else {
223        $declarations{".objc_class_name_$name"} = $availability;
224    }
225}
226
227# All exported symbols must have an export declaration
228my @missing_symbols;
229for my $name (keys %symbols) {
230    my $avail = $declarations{$name};
231    if ($avail eq "unavailable"  ||  !defined $avail) {
232        push @missing_symbols, $name;
233    }
234}
235for my $symbol (sort @missing_symbols) {
236    print "BAD: symbol $symbol has no export declaration\n"; $bad++;
237}
238
239
240# All export declarations must have an exported symbol
241my @missing_decls;
242for my $name (keys %declarations) {
243    my $avail = $declarations{$name};
244    my $hasSymbol = exists $symbols{$name};
245    if ($avail ne "unavailable"  &&  !$hasSymbol) {
246        push @missing_decls, $name;
247    }
248}
249for my $decl (sort @missing_decls) {
250    print "BAD: declaration $decl has no exported symbol\n"; $bad++;
251}
252
253print "OK: verify-exports\n" unless $bad;
254exit ($bad ? 1 : 0);
255