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