interface_cmp.pl revision 10355:26c444652867
1#!/usr/bin/perl -w
2#
3# CDDL HEADER START
4#
5# The contents of this file are subject to the terms of the
6# Common Development and Distribution License (the "License").
7# You may not use this file except in compliance with the License.
8#
9# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10# or http://www.opensolaris.org/os/licensing.
11# See the License for the specific language governing permissions
12# and limitations under the License.
13#
14# When distributing Covered Code, include this CDDL HEADER in each
15# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16# If applicable, add the following below this CDDL HEADER, with the
17# fields enclosed by brackets "[]" replaced with your own identifying
18# information: Portions Copyright [yyyy] [name of copyright owner]
19#
20# CDDL HEADER END
21#
22
23#
24# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
25# Use is subject to license terms.
26#
27
28#
29# interface_cmp audits two interface definition files (as created by
30# interface_check) against one another, and confirms that:
31#
32#  o	All versioned libraries that were present in the previous interface
33#	are present in the new interface
34#
35#  o	for each non-private interface in a library confirm that no symbols
36#	have been removed and that no symbols have been added to it between
37#	the two revisions
38#
39# Return codes:
40#
41#  0	All interfaces in the new release are identical in old release.
42#  1	Something is different refer to the error messages.
43
44
45use strict;
46
47use POSIX qw(getenv);
48use Getopt::Std;
49use File::Basename;
50
51#### Define all global variables (required for strict)
52use vars  qw($Prog);
53use vars  qw(%opt);
54use vars  qw(%old_hash %old_alias %new_hash %new_alias);
55
56# Exception Arrays:
57#
58# The ADDSYM and DELSYM exceptions are maintained on the @AddSymList
59# and @DelSymList arrays, respectively. Each array element is a reference
60# to a subarray of triples:
61#	(sym_re, ver_re, obj_re)
62# where each item in the tripple is a regular expression, used to
63# match a particular symbol/version/object combination.
64#
65# The EMPTY_TOPVERSION exceptions are maintained on the @EmptyTopVerList
66# array. Each array element is a reference to a subarray of pairs:
67#	(ver_re, obj_re)
68# where each item in the pair is a regular expression, used to
69# match a particular version/object combination.
70#
71use vars  qw(@AddSymList @DelSymList @EmptyTopVerList);
72
73
74## LoadExceptions
75#
76# Locate the exceptions file and process its contents. We can't use
77# onbld_elfmod::LoadExceptionsToEXRE() for this, because our exceptions
78# need to support more than a single regular expression.
79#
80# exit:
81#	@AddSymList, @DelSymList, and @EmptyTopVerList have been updated
82#
83# note:
84#	We expand strings of the form MACH(dir) to match the given
85#	directory as well as any 64-bit architecture subdirectory that
86#	might be present (i.e. amd64, sparcv9).
87#
88sub LoadExceptions {
89	my $file;
90	my $Line;
91	my $LineNum = 0;
92	my $err = 0;
93
94	# Locate the exception file
95	FILE: {
96		# If -e is specified, that file must be used
97		if ($opt{e}) {
98			$file = $opt{e};
99			last FILE;
100		}
101
102		# If this is an activated workspace, use the exception
103		# file found in the exceptions_list directory.
104		if (defined($ENV{CODEMGR_WS})) {
105			$file = "$ENV{CODEMGR_WS}/exception_lists/interface_cmp";
106			last FILE if (-f $file);
107		}
108
109		# As a final backstop, the SUNWonbld package provides a
110		# copy of the exception file. This can be useful if we
111		# are being used with an older workspace.
112		#
113		# This script is installed in the SUNWonbld bin directory,
114		# while the exception file is in etc/exception_lists. Find
115		# it relative to the script location given by $0.
116		$file = dirname($0) . "/../etc/exception_lists/interface_cmp";
117		last FILE if (-f $file);
118
119		# No exception file was found.
120		return;
121	}
122
123	open (EFILE, $file) ||
124		die "$Prog: unable to open exceptions file: $file";
125	while ($Line = onbld_elfmod::GetLine(\*EFILE, \$LineNum)) {
126
127		# Expand MACH()
128		$Line =~ s/MACH\(([^)]+)\)/$1(\/amd64|\/sparcv9)?/g;
129
130		if ($Line =~ /^DELSYM\s+/) {
131		    my ($item, $sym_re, $ver_re, $obj_re) =
132			split(/\s+/, $Line, 4);
133		    push @DelSymList, [ $sym_re, $ver_re, $obj_re ];
134		    next;
135		}
136
137		if ($Line =~ /^ADDSYM\s+/) {
138		    my ($item, $sym_re, $ver_re, $obj_re) =
139			split(/\s+/, $Line, 4);
140		    push @AddSymList, [ $sym_re, $ver_re, $obj_re ];
141		    next;
142		}
143
144		if ($Line =~ /^EMPTY_TOPVERSION\s+/) {
145		    my ($item, $ver_re, $obj_re) = split(/\s+/, $Line, 3);
146		    push @EmptyTopVerList, [ $ver_re, $obj_re ];
147		    next;
148		}
149
150		$err++;
151		printf(STDERR "$file: Unrecognized option: ".
152		    "line $LineNum: $Line\n");
153	}
154	close EFILE;
155
156	exit 1 if ($err != 0);
157}
158
159## ExSym(SymList, sym, ver, obj)
160#
161# Compare a given symbol/version/object combination against the
162# exceptions found in the given list.
163#
164# entry:
165#	SymList - Reference to @AddSymList, or @DelSymList.
166#	sym, ver, obj - Combination to be compared against exception list
167#
168# exit:
169#	Returns True (1) if there is a match, and False (0) otherwise.
170#
171sub ExSym {
172	my ($SymList, $sym, $ver, $obj) = @_;
173
174	foreach my $ex (@$SymList) {
175		return 1 if ($obj =~ /$$ex[2]/) && ($ver =~ /$$ex[1]/) &&
176		    ($sym =~ /$$ex[0]/);
177	}
178
179	return 0;
180}
181
182## ExTopVer(ver, obj)
183#
184# Compare a given version/object combination against the pairs found
185# in @EmptyTopVerList.
186#
187# entry:
188#	ver, obj - Combination to be compared against empty top version list
189#
190# exit:
191#	Returns True (1) if there is a match, and False (0) otherwise.
192#
193sub ExTopVer {
194	my ($ver, $obj) = @_;
195
196	foreach my $ex (@EmptyTopVerList) {
197		return 1 if ($obj =~ /$$ex[1]/) && ($ver =~ /$$ex[0]/);
198	}
199
200	return 0;
201}
202
203## ExpandInheritance(objhashref)
204#
205# For each version contained in the specified object hash reference,
206# add the inherited symbols.
207#
208sub ExpandInheritance {
209	my $obj = $_[0];
210
211	# Versions to process. Typically, inheriting versions come before
212	# the versions they inherit. Processing the list in reverse order
213	# maximizes the odds that a needed sub-version will have already
214	# have been processed.
215	my @vers = reverse(@{$obj->{'VERSION_NAMES'}});
216
217	# Versions to process in the next pass
218	my @next_vers = ();
219
220	# Hash, indexed by version name, that reflects whether the version
221	# has been expanded yet or not.
222	my %done = ();
223
224	while (scalar(@vers) > 0) {
225		foreach my $name (@vers) {
226			my $i;
227			my $defer = 0;
228			my $cur_version = $obj->{'VERSION_INFO'}{$name};
229			my ($top, $direct, $total, $symhash, $inheritarr) =
230			    @{$cur_version};
231
232			# In order to expand this version, all the inherited
233			# versions must already have been done. If not, put
234			# this version on @next_vers for the next pass.
235			my $num = scalar(@$inheritarr);
236			for ($i = 0; $i < $num; $i++) {
237			    if (!$done{$inheritarr->[$i]}) {
238				$defer = 1;
239				push @next_vers, $name;
240				last;
241			    }
242			}
243			next if ($defer);
244
245			# Add all the symbols from the inherited versions
246			# to this one.
247			for ($i = 0; $i < $num; $i++) {
248				my $i_version =
249				    $obj->{'VERSION_INFO'}{$inheritarr->[$i]};
250				my $i_symhash = $i_version->[3];
251
252				foreach my $sym (keys %$i_symhash) {
253				    if (!defined($cur_version->[3]{$sym})) {
254					    $cur_version->[2]++;
255					    $cur_version->[3]{$sym} = 'INHERIT';
256				    }
257				}
258			}
259
260			$done{$name} = 1;
261		}
262
263		@vers = @next_vers;
264		@next_vers = ();
265	}
266}
267
268## ReadInterface(file, alias)
269#
270# Read the interface description file, as produced by interface_check, and
271# return a hash describing it.
272#
273# entry:
274#	file - Interface file to read.
275#	alias - Refence to hash to be filled in with any aliases
276#		that are seen in the file. The alias name is the key,
277#		and the object is the value.
278#
279# exit:
280#	The hash referenced by alias has been updated.
281#
282#	The return value is a hash that encapsulates the interface
283#	information. This hash returned uses the object names as the
284#	key. Each key references a sub-hash that contains information
285#	for that object:
286#
287#	CLASS		-> ELFCLASS
288#	TYPE		-> ELF type
289#	VERSION_NAMES	-> Reference to array [1..n] of version names, in the
290#			   order they come from the input file.
291#	VERSION_INFO	-> Reference to hash indexed by version name, yielding
292#			   a reference to an array containing information about
293#			   that version.
294#
295#	The arrays referenced via VERSION_INFO are of the form:
296#
297#		(top, new, total, symhashref, inheritarrref)
298#
299#	where:
300#		top - 1 if version is a TOP_VERSION, 0 for a regular VERSION
301#		new - Number of symbols defined explicitly by version
302#		total - Number of symbols included in version, both new,
303#			and via inheritance.
304#		symhashref - Reference to hash indexed by symbol names, and
305#			yielding true (1).
306#		inheritarrref - Reference to array of names of versions
307#			inherited by this one.
308#
309sub ReadInterface {
310	my ($file, $alias) = @_;
311	my %main_hash = ();
312	my $Line;
313	my $LineNum = 0;
314	my $obj_name;
315	my $obj_hash;
316	my $sym_ok = 0;
317	my $cur_version;
318
319	open(FILE, $file) || die "$Prog: Unable to open: $file";
320
321	# Until we see an OBJECT line, nothing else is valid. To
322	# simplify the error handling, use a simple initial loop to
323	# read the file up to that point
324	while ($Line = onbld_elfmod::GetLine(\*FILE, \$LineNum)) {
325		if ($Line =~ s/^OBJECT\s+//i) {
326		    $obj_name = $Line;
327		    $main_hash{$obj_name} = {};
328		    $obj_hash = $main_hash{$obj_name};
329		    last;
330		}
331		die "$file: OBJECT expected on line $LineNum: $Line\n";
332	}
333
334	# Read the remainder of the file
335	while ($Line = onbld_elfmod::GetLine(\*FILE, \$LineNum)) {
336		# Items are parsed in order of decreasing frequency
337
338		if ($Line =~
339		    /^SYMBOL\s+([^\s]+)$/i) {
340			my $sym = $1;
341
342			die "$file: SYMBOL not expected on line $LineNum: $Line\n"
343			    if !$sym_ok;
344
345			$cur_version->[1]++;
346			$cur_version->[2]++;
347			$cur_version->[3]{$sym} = 'NEW';
348			next;
349		}
350
351		if ($Line =~ /^((TOP_)?VERSION)\s+([^\s]+)(\s+\{(.*)\})?\s*$/i) {
352			my ($top, $name, $inherit) = ($2, $3, $5);
353
354			$top = defined($top) ? 1 : 0;
355
356			my @inheritarr = defined($inherit) ?
357			    split /[,{\s]+/, $inherit : ();
358
359			$cur_version = [ $top, 0, 0, {}, \@inheritarr ];
360			$obj_hash->{'VERSION_INFO'}{$name} = $cur_version;
361
362			push @{$obj_hash->{'VERSION_NAMES'}}, $name;
363			$sym_ok = 1;
364			next;
365		}
366
367		if ($Line =~ /^OBJECT\s+([^\s]+)$/i) {
368		    my $prev_obj_hash = $obj_hash;
369		    $obj_name = $1;
370		    $main_hash{$obj_name} = {};
371		    $obj_hash = $main_hash{$obj_name};
372
373		    # Expand the versions for the object just processed
374		    ExpandInheritance($prev_obj_hash);
375		    next;
376		}
377
378		if ($Line =~ /^CLASS\s+([^\s]+)$/i) {
379			$obj_hash->{'CLASS'} = $1;
380			next;
381		}
382
383		if ($Line =~ /^TYPE\s+([^\s]+)$/i) {
384			$obj_hash->{'TYPE'} = $1;
385			next;
386		}
387
388		if ($Line =~ /^ALIAS\s+([^\s]+)$/i) {
389			$$alias{$1} = $obj_name;
390			next;
391		}
392
393		die "$file: unrecognized item on line $LineNum: $Line\n";
394	}
395	close FILE;
396
397	# Expand the versions for the final object from the file
398	ExpandInheritance($obj_hash);
399
400	return %main_hash;
401}
402
403## PrintInterface(main_hash, alias)
404#
405# Dump the contents of main_hash and alias to stdout in the same format
406# used by interface_check to produce the input interface file. This output
407# should diff cleanly against the original (ignoring the header comments).
408#
409sub PrintInterface {
410	my ($main_hash, $alias_hash) = @_;
411
412	foreach my $obj (sort keys %$main_hash) {
413		print "OBJECT\t$obj\n";
414		print "CLASS\t$main_hash->{$obj}{'CLASS'}\n";
415		print "TYPE\t$main_hash->{$obj}{'TYPE'}\n";
416
417		# This is inefficient, but good enough for debugging
418		# Look at all the aliases and print those that belong
419		# to this object.
420		foreach my $alias (sort keys %$alias_hash) {
421			print "ALIAS\t$alias\n"
422			    if ($obj eq $alias_hash->{$alias});
423		}
424
425		next if !defined($main_hash->{$obj}{'VERSION_NAMES'});
426
427		my $num = scalar(@{$main_hash->{$obj}{'VERSION_NAMES'}});
428		my $i;
429		for ($i = 0; $i < $num; $i++) {
430			my $name = $main_hash->{$obj}{'VERSION_NAMES'}[$i];
431			my ($top, $direct, $total, $symhash, $inheritarr) =
432			    @{$main_hash->{$obj}{'VERSION_INFO'}{$name}};
433
434			$top = $top ? "TOP_" : '';
435
436			my $inherit = (scalar(@$inheritarr) > 0) ?
437			    "\t{" . join(', ', @{$inheritarr}) . "}" : '';
438
439			print "${top}VERSION\t$name$inherit\n";
440
441			foreach my $sym (sort keys %$symhash) {
442				print "\t$symhash->{$sym}\t$sym\n";
443			}
444		}
445	}
446}
447
448## compare()
449#
450# Compare the old interface definition contained in (%old_hash, %old_alias)
451# with the new interface contained in (%new_hash, %new_alias).
452#
453sub compare {
454	foreach my $old_obj (sort keys %old_hash) {
455		my $new_obj = $old_obj;
456		my $Ttl = 0;
457
458		# If the object does not exist in the new interface,
459		# then see if there's an alias for it. Failing that,
460		# we simply ignore the object.
461		if (!defined($new_hash{$new_obj})) {
462			next if !defined($new_alias{$new_obj});
463			$new_obj = $new_alias{$new_obj};
464		}
465
466		my $old = $old_hash{$old_obj};
467		my $new = $new_hash{$new_obj};
468
469		# Every version in the old object must exist in the new object,
470		# and there must be exactly the same symbols in each.
471		my $num = scalar(@{$old->{'VERSION_NAMES'}});
472		for (my $i = 0; $i < $num; $i++) {
473			my $name = $old->{'VERSION_NAMES'}[$i];
474
475			# New object must have this version
476			if (!defined($new->{'VERSION_INFO'}{$name})) {
477				onbld_elfmod::OutMsg2(\*STDOUT, \$Ttl, $old_obj,
478				    $new_obj, "$name: deleted version");
479				next;
480			}
481
482			my ($old_top, $old_direct, $old_total, $old_symhash) =
483			    @{$old->{'VERSION_INFO'}{$name}};
484			my ($new_top, $new_direct, $new_total, $new_symhash) =
485			    @{$new->{'VERSION_INFO'}{$name}};
486
487			# If this is an empty top version, and the old object
488			# has the EMPTY_TOPVERSION exception set, then we
489			# skip it as if it were not present.
490			next if $old_top && ($old_direct == 0) &&
491			    ExTopVer($name, $old_obj);
492
493			# We check that every symbol in the old object is
494			# in the new one to detect deleted symbols. We then
495			# check that every symbol in the new object is also
496			# in the old object, to find added symbols. If the
497			# "deleted" check is clean, and the two objects have
498			# the same number of symbols in their versions, then we
499			# can skip the "added" test, because we know that
500			# there is no room for an addition to have happened.
501			# Since most objects satisfy these constraints, we
502			# end up doing roughly half the number of comparisons
503			# that would otherwise be needed.
504			my $check_added_syms =
505			    ($old_total == $new_total) ? 0: 1;
506
507			# Every symbol in the old version must be in the new one
508			foreach my $sym (sort keys %$old_symhash) {
509				if (!defined($new_symhash->{$sym})) {
510					onbld_elfmod::OutMsg2(\*STDOUT,
511					   \$Ttl, $old_obj, $new_obj,
512					   "$name: deleted interface: $sym")
513					    if !ExSym(\@DelSymList,
514						      $sym, $name, $new_obj);
515					$check_added_syms = 1;
516				}
517			}
518
519			# Do the "added" check, unless we can optimize it away.
520			# Every symbol in the new version must be in the old one.
521			if ($check_added_syms) {
522				foreach my $sym (sort keys %$new_symhash) {
523				    if (!defined($old_symhash->{$sym})) {
524					next if ExSym(\@AddSymList,
525					    $sym, $name, $new_obj);
526					onbld_elfmod::OutMsg2(\*STDOUT,
527					       \$Ttl, $old_obj, $new_obj,
528					       "$name: added interface: $sym");
529				    }
530				}
531			}
532
533			# We want to ensure that version numbers in an
534			# inheritance chain don't go up by more than 1 in
535			# any given release. If the version names are in the
536			# standard SUNW_x.y[.z] format, we can compare the
537			# two top versions and see if this has happened.
538			#
539			# For a given SUNW_x.y[.z], valid sucessors would
540			# be SUNW_x.(y+1) or SUNW_x.y.(z+1), where z is
541			# assumed to be 0 if not present.
542			#
543			# This check only makes sense when the new interface
544			# is a direct decendent of the old one, as specified
545			# via the -d option. If the two interfaces are more
546			# than one release apart, we should not do this test.
547			if ($opt{d} && $old_top && !$new_top &&
548			    ($name =~ /^SUNW_(\d+)\.(\d+)(\.(\d+))?/)) {
549				my $iname1 = "SUNW_$1." . ($2 + 1);
550				my $iname2;
551				if (defined($4)) {
552			    		$iname2 = "SUNW_$1.$2." . ($4 + 1);
553				} else {
554			    		$iname2 = "SUNW_$1.$2.1";
555				}
556
557				if (defined($new->{'VERSION_INFO'}{$iname1}) ||
558				    defined($new->{'VERSION_INFO'}{$iname2})) {
559					my $i_top =
560					    $new->{'VERSION_INFO'}{$iname1}[0] ||
561					    $new->{'VERSION_INFO'}{$iname2}[0];
562					if (!$i_top) {
563						onbld_elfmod::OutMsg2(\*STDOUT,
564						    \$Ttl, $old_obj, $new_obj,
565						    "$name: inconsistant " .
566						    "version increment: " .
567						    "expect $iname1 or $iname2 ".
568						    "to replace top version");
569					}
570				} else {
571 					onbld_elfmod::OutMsg2(\*STDOUT,
572					    \$Ttl, $old_obj, $new_obj,
573				            "$name: expected superseding " .
574					    "top version to $name not " .
575					    "present: $iname1 or $iname2");
576				}
577			}
578		}
579
580
581		# Empty versions in the established interface description
582		# are usually the result of fixing a versioning mistake
583		# at some point in the past. These versions are part of
584		# the public record, and cannot be changed now. However, if
585		# comparing two interface descriptions from the same gate,
586		# flag any empty versions in the new interface description
587		# that are not present in the old one. These have yet to
588		# become part of the official interface, and should be removed
589		# before they do.
590		next if !$opt{d};
591
592		$num = scalar(@{$new->{'VERSION_NAMES'}});
593		for (my $i = 0; $i < $num; $i++) {
594			my $name = $new->{'VERSION_NAMES'}[$i];
595
596			# If old object has this version, skip it
597			next if defined($old->{'VERSION_INFO'}{$name});
598
599			# If explicitly whitelisted, skip it
600			next if ExTopVer($name, $new_obj);
601
602			my ($new_top, $new_direct, $new_total, $new_symhash) =
603			    @{$new->{'VERSION_INFO'}{$name}};
604
605			if ($new_direct == 0) {
606				onbld_elfmod::OutMsg2(\*STDOUT,
607				    \$Ttl, $old_obj, $new_obj,
608				    "$name: invalid empty new version");
609			}
610		}
611	}
612
613}
614
615
616
617# -----------------------------------------------------------------------------
618
619# Establish a program name for any error diagnostics.
620chomp($Prog = `basename $0`);
621
622# The onbld_elfmod package is maintained in the same directory as this
623# script, and is installed in ../lib/perl. Use the local one if present,
624# and the installed one otherwise.
625my $moddir = dirname($0);
626$moddir = "$moddir/../lib/perl" if ! -f "$moddir/onbld_elfmod.pm";
627require "$moddir/onbld_elfmod.pm";
628
629# Check that we have arguments. Normally, 2 plain arguments are required,
630# but if -t is present, only one is allowed.
631if ((getopts('de:ot', \%opt) == 0) || (scalar(@ARGV) != ($opt{t} ? 1 : 2))) {
632	print "usage: $Prog [-dot] [-e exfile] old new\n";
633	print "\t[-d]\t\tnew is a direct decendent of old\n";
634	print "\t[-e exfile]\texceptions file\n";
635	print "\t[-o]\t\tproduce one-liner output (prefixed with pathname)\n";
636	print "\t[-t]\tParse old, and recreate to stdout\n";
637	exit 1;
638}
639
640# Locate and process the exceptions file
641LoadExceptions();
642
643%old_alias = ();
644%old_hash = ReadInterface($ARGV[0], \%old_alias);
645
646# If -t is present, only one argument is allowed --- we parse it, and then
647# print the same information back to stderr in the same format as the original.
648# This is useful for debugging, to verify that the parsing is correct.
649if ($opt{t}) {
650	PrintInterface(\%old_hash, \%old_alias);
651	exit 0;
652}
653
654%new_alias = ();
655%new_hash = ReadInterface($ARGV[1], \%new_alias);
656
657compare();
658
659exit 0;
660