xref: /illumos-gate/usr/src/tools/scripts/wsdiff.py (revision e74a1fb927623517f38eb4d4ccab4b4869949ba4)
1#!/usr/bin/python2.4
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# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
23# Use is subject to license terms.
24#
25
26#
27# wsdiff(1) is a tool that can be used to determine which compiled objects
28# have changed as a result of a given source change. Developers backporting
29# new features, RFEs and bug fixes need to be able to identify the set of
30# patch deliverables necessary for feature/fix realization on a patched system.
31#
32# The tool works by comparing objects in two trees/proto areas (one build with,
33# and without the source changes.
34#
35# Using wsdiff(1) is fairly simple:
36#	- Bringover to a fresh workspace
37#	- Perform a full non-debug build (clobber if workspace isn't fresh)
38#	- Move the proto area aside, call it proto.old, or something.
39#	- Integrate your changes to the workspace
40#	- Perform another full non-debug clobber build.
41#	- Use wsdiff(1) to see what changed:
42#		$ wsdiff proto.old proto
43#
44# By default, wsdiff will print the list of changed objects / deliverables to
45# stdout. If a results file is specified via -r, the list of differing objects,
46# and details about why wsdiff(1) thinks they are different will be logged to
47# the results file.
48#
49# By invoking nightly(1) with the -w option to NIGHTLY_FLAGS, nightly(1) will use
50# wsdiff(1) to report on what objects changed since the last build.
51#
52# For patch deliverable purposes, it's advised to have nightly do a clobber,
53# non-debug build.
54#
55# Think about the results. Was something flagged that you don't expect? Go look
56# at the results file to see details about the differences.
57#
58# Use the -i option in conjunction with -v and -V to dive deeper and have wsdiff(1)
59# report with more verbosity.
60#
61# Usage: wsdiff [-vVt] [-r results ] [-i filelist ] old new
62#
63# Where "old" is the path to the proto area build without the changes, and
64# "new" is the path to the proto area built with the changes. The following
65# options are supported:
66#
67#        -v      Do not truncate observed diffs in results
68#        -V      Log *all* ELF sect diffs vs. logging the first diff found
69#        -t      Use onbld tools in $SRC/tools
70#        -r      Log results and observed differences
71#        -i      Tell wsdiff which objects to compare via an input file list
72
73import datetime, fnmatch, getopt, profile, os, popen2, commands
74import re, select, string, struct, sys, tempfile, time
75from stat import *
76
77# Human readable diffs truncated by default if longer than this
78# Specifying -v on the command line will override
79diffs_sz_thresh = 4096
80
81# Default search path for wsdiff
82wsdiff_path = [ "/usr/bin",
83		"/usr/ccs/bin",
84		"/lib/svc/bin",
85		"/opt/onbld/bin" ]
86
87# These are objects that wsdiff will notice look different, but will not report.
88# Existence of an exceptions list, and adding things here is *dangerous*,
89# and therefore the *only* reasons why anything would be listed here is because
90# the objects do not build deterministically, yet we *cannot* fix this.
91#
92# These perl libraries use __DATE__ and therefore always look different.
93# Ideally, we would purge use the use of __DATE__ from the source, but because
94# this is source we wish to distribute with Solaris "unchanged", we cannot modify.
95#
96wsdiff_exceptions = [ "usr/perl5/5.8.4/lib/sun4-solaris-64int/CORE/libperl.so.1",
97		      "usr/perl5/5.6.1/lib/sun4-solaris-64int/CORE/libperl.so.1",
98		      "usr/perl5/5.8.4/lib/i86pc-solaris-64int/CORE/libperl.so.1",
99		      "usr/perl5/5.6.1/lib/i86pc-solaris-64int/CORE/libperl.so.1"
100		      ]
101
102#####
103# Logging routines
104#
105
106# Informational message to be printed to the screen, and the log file
107def info(msg) :
108
109	print >> sys.stdout, msg
110	if logging :
111		print >> log, msg
112	sys.stdout.flush()
113
114# Error message to be printed to the screen, and the log file
115def error(msg) :
116
117	print >> sys.stderr, "ERROR:", msg
118	sys.stderr.flush()
119	if logging :
120		print >> log, "ERROR:", msg
121		log.flush()
122
123# Informational message to be printed only to the log, if there is one.
124def v_info(msg) :
125
126	if logging :
127		print >> log, msg
128		log.flush()
129
130#
131# Flag a detected file difference
132# Display the fileName to stdout, and log the difference
133#
134def difference(f, dtype, diffs) :
135
136	if f in wsdiff_exceptions :
137		return
138
139	print >> sys.stdout, f
140	sys.stdout.flush()
141
142	log_difference(f, dtype, diffs)
143
144#
145# Do the actual logging of the difference to the results file
146#
147def log_difference(f, dtype, diffs) :
148	if logging :
149		print >> log, f
150		print >> log, "NOTE:", dtype, "difference detected."
151
152		difflen = len(diffs)
153		if difflen > 0 :
154			print >> log
155
156			if not vdiffs and difflen > diffs_sz_thresh :
157				print >> log, diffs[:diffs_sz_thresh]
158				print >> log, \
159				      "... truncated due to length: " \
160				      "use -v to override ..."
161			else :
162				print >> log, diffs
163			print >> log, "\n"
164		log.flush()
165
166
167#####
168# diff generating routines
169#
170
171#
172# Return human readable diffs from two temporary files
173#
174def diffFileData(tmpf1, tmpf2) :
175
176	# Filter the data through od(1) if the data is detected
177	# as being binary
178	if isBinary(tmpf1) or isBinary(tmpf2) :
179		tmp_od1 = tmpf1 + ".od"
180		tmp_od2 = tmpf2 + ".od"
181
182		cmd = od_cmd + " -c -t x4" + " " + tmpf1 + " > " + tmp_od1
183		os.system(cmd)
184		cmd = od_cmd + " -c -t x4" + " " + tmpf2 + " > " + tmp_od2
185		os.system(cmd)
186
187		tmpf1 = tmp_od1
188		tmpf2 = tmp_od2
189
190	data = commands.getoutput(diff_cmd + " " + tmpf1 + " " + tmpf2)
191
192	return data
193
194#
195# Return human readable diffs betweeen two datasets
196#
197def diffData(d1, d2) :
198
199	global tmpFile1
200	global tmpFile2
201
202	try:
203		fd1 = open(tmpFile1, "w")
204	except:
205		error("failed to open: " + tmpFile1)
206		cleanup(1)
207	try:
208		fd2 = open(tmpFile2, "w")
209	except:
210		error("failed to open: " + tmpFile2)
211		cleanup(1)
212
213	fd1.write(d1)
214	fd2.write(d2)
215	fd1.close()
216	fd2.close()
217
218	return diffFileData(tmpFile1, tmpFile2)
219
220#####
221# Misc utility functions
222#
223
224# Prune off the leading prefix from string s
225def str_prefix_trunc(s, prefix) :
226	snipLen = len(prefix)
227	return s[snipLen:]
228
229#
230# Prune off leading proto path goo (if there is one) to yield
231# the deliverable's eventual path relative to root
232# e.g. proto.base/root_sparc/usr/src/cmd/prstat => usr/src/cmd/prstat
233#
234def fnFormat(fn) :
235	root_arch_str = "root_" + arch
236
237	pos = fn.find(root_arch_str)
238	if pos == -1 :
239		return fn
240
241	pos = fn.find("/", pos)
242	if pos == -1 :
243		return fn
244
245	return fn[pos + 1:]
246
247#####
248# Usage / argument processing
249#
250
251#
252# Display usage message
253#
254def usage() :
255	sys.stdout.flush()
256	print >> sys.stderr, """Usage: wsdiff [-vVt] [-r results ] [-i filelist ] old new
257        -v      Do not truncate observed diffs in results
258        -V      Log *all* ELF sect diffs vs. logging the first diff found
259        -t      Use onbld tools in $SRC/tools
260        -r      Log results and observed differences
261        -i      Tell wsdiff which objects to compare via an input file list"""
262	sys.exit(1)
263
264#
265# Process command line options
266#
267def args() :
268
269	global logging
270	global vdiffs
271	global reportAllSects
272
273	validOpts = 'i:r:vVt?'
274
275	baseRoot = ""
276	ptchRoot = ""
277	fileNamesFile = ""
278	results = ""
279	localTools = False
280
281	# getopt.getopt() returns:
282	#	an option/value tuple
283	#	a list of remaining non-option arguments
284	#
285	# A correct wsdiff invocation will have exactly two non option
286	# arguments, the paths to the base (old), ptch (new) proto areas
287	try:
288		optlist, args = getopt.getopt(sys.argv[1:], validOpts)
289	except getopt.error, val:
290		usage()
291
292	if len(args) != 2 :
293		usage();
294
295	for opt,val in optlist :
296		if opt == '-i' :
297			fileNamesFile = val
298		elif opt == '-r' :
299			results = val
300			logging = True
301		elif opt == '-v' :
302			vdiffs = True
303		elif opt == '-V' :
304			reportAllSects = True
305		elif opt == '-t':
306			localTools = True
307		else:
308			usage()
309
310	baseRoot = args[0]
311	ptchRoot = args[1]
312
313	if len(baseRoot) == 0 or len(ptchRoot) == 0 :
314		usage()
315
316	if logging and len(results) == 0 :
317		usage()
318
319	if vdiffs and not logging :
320		error("The -v option requires a results file (-r)")
321		sys.exit(1)
322
323	if reportAllSects and not logging :
324		error("The -V option requires a results file (-r)")
325		sys.exit(1)
326
327	# alphabetical order
328	return	baseRoot, fileNamesFile, localTools, ptchRoot, results
329
330#####
331# File identification
332#
333
334#
335# Identify the file type.
336# If it's not ELF, use the file extension to identify
337# certain file types that require special handling to
338# compare. Otherwise just return a basic "ASCII" type.
339#
340def getTheFileType(f) :
341
342	extensions = { 'a'	:	'ELF Object Archive',
343		       'jar'	:	'Java Archive',
344		       'html'	:	'HTML',
345		       'ln'	:	'Lint Library',
346		       'esa'	:	'Elfsign Activation',
347		       'db'	:	'Sqlite Database' }
348
349	try:
350		if os.stat(f)[ST_SIZE] == 0 :
351			return 'ASCII'
352	except:
353		error("failed to stat " + f)
354		return 'Error'
355
356	if isELF(f) == 1 :
357		return 'ELF'
358
359	fnamelist = f.split('.')
360	if len(fnamelist) > 1 :	# Test the file extension
361		extension = fnamelist[-1]
362		if extension in extensions.keys():
363			return extensions[extension]
364
365	return 'ASCII'
366
367#
368# Return non-zero if "f" is an ELF file
369#
370elfmagic = '\177ELF'
371def isELF(f) :
372	try:
373		fd = open(f)
374	except:
375		error("failed to open: " + f)
376		return 0
377	magic = fd.read(len(elfmagic))
378	fd.close()
379
380	if magic == elfmagic :
381		return 1
382	return 0
383
384#
385# Return non-zero is "f" is binary.
386# Consider the file to be binary if it contains any null characters
387#
388def isBinary(f) :
389	try:
390		fd = open(f)
391	except:
392		error("failed to open: " + f)
393		return 0
394	s = fd.read()
395	fd.close()
396
397	if s.find('\0') == -1 :
398		return 0
399	else :
400		return 1
401
402#####
403# Directory traversal and file finding
404#
405
406#
407# Return a sorted list of files found under the specified directory
408#
409def findFiles(d) :
410	for path, subdirs, files in os.walk(d) :
411		files.sort()
412		for name in files :
413			yield os.path.join(path, name)
414
415#
416# Examine all files in base, ptch
417#
418# Return a list of files appearing in both proto areas,
419# a list of new files (files found only in ptch) and
420# a list of deleted files (files found only in base)
421#
422def protoCatalog(base, ptch) :
423	compFiles = []		# List of files in both proto areas
424	ptchList = []		# List of file in patch proto area
425
426	newFiles = []		# New files detected
427	deletedFiles = []	# Deleted files
428
429	baseFilesList = list(findFiles(base))
430	baseStringLength = len(base)
431
432	ptchFilesList = list(findFiles(ptch))
433	ptchStringLength = len(ptch)
434
435	# Inventory files in the base proto area
436	for fn in baseFilesList :
437		if os.path.islink(fn) :
438			continue
439
440		fileName = fn[baseStringLength:]
441		compFiles.append(fileName)
442
443	# Inventory files in the patch proto area
444	for fn in ptchFilesList :
445		if os.path.islink(fn) :
446			continue
447
448		fileName = fn[ptchStringLength:]
449		ptchList.append(fileName)
450
451	# Deleted files appear in the base area, but not the patch area
452	for fileName in compFiles :
453		if not fileName in ptchList :
454			deletedFiles.append(fileName)
455
456	# Eliminate "deleted" files from the list of objects appearing
457	# in both the base and patch proto areas
458	for fileName in deletedFiles :
459		try:
460		       	compFiles.remove(fileName)
461		except:
462			error("filelist.remove() failed")
463
464	# New files appear in the patch area, but not the base
465	for fileName in ptchList :
466		if not fileName in compFiles :
467			newFiles.append(fileName)
468
469	return compFiles, newFiles, deletedFiles
470
471#
472# Examine the files listed in the input file list
473#
474# Return a list of files appearing in both proto areas,
475# a list of new files (files found only in ptch) and
476# a list of deleted files (files found only in base)
477#
478def flistCatalog(base, ptch, flist) :
479	compFiles = []		# List of files in both proto areas
480	newFiles = []		# New files detected
481	deletedFiles = []	# Deleted files
482
483	try:
484		fd = open(flist, "r")
485	except:
486		error("could not open: " + flist)
487		cleanup(1)
488
489	files = []
490	files = fd.readlines()
491
492	for f in files :
493		ptch_present = True
494		base_present = True
495
496		if f == '\n' :
497			continue
498
499		# the fileNames have a trailing '\n'
500		f = f.rstrip()
501
502		# The objects in the file list have paths relative
503		# to $ROOT or to the base/ptch directory specified on
504		# the command line.
505		# If it's relative to $ROOT, we'll need to add back the
506		# root_`uname -p` goo we stripped off in fnFormat()
507		if os.path.exists(base + f) :
508			fn = f;
509		elif os.path.exists(base + "root_" + arch + "/" + f) :
510			fn = "root_" + arch + "/" + f
511		else :
512			base_present = False
513
514		if base_present :
515			if not os.path.exists(ptch + fn) :
516				ptch_present = False
517		else :
518			if os.path.exists(ptch + f) :
519				fn = f
520			elif os.path.exists(ptch + "root_" + arch + "/" + f) :
521				fn = "root_" + arch + "/" + f
522			else :
523				ptch_present = False
524
525		if os.path.islink(base + fn) :	# ignore links
526			base_present = False
527		if os.path.islink(ptch + fn) :
528			ptch_present = False
529
530		if base_present and ptch_present :
531			compFiles.append(fn)
532		elif base_present :
533			deletedFiles.append(fn)
534		elif ptch_present :
535			newFiles.append(fn)
536		else :
537			if os.path.islink(base + fn) and os.path.islink(ptch + fn) :
538				continue
539			error(f + " in file list, but not in either tree. Skipping...")
540
541	return compFiles, newFiles, deletedFiles
542
543
544#
545# Build a fully qualified path to an external tool/utility.
546# Consider the default system locations. For onbld tools, if
547# the -t option was specified, we'll try to use built tools in $SRC tools,
548# and otherwise, we'll fall back on /opt/onbld/
549#
550def find_tool(tool) :
551
552	# First, check what was passed
553	if os.path.exists(tool) :
554		return tool
555
556	# Next try in wsdiff path
557	for pdir in wsdiff_path :
558		location = pdir + "/" + tool
559		if os.path.exists(location) :
560			return location + " "
561
562		location = pdir + "/" + arch + "/" + tool
563		if os.path.exists(location) :
564			return location + " "
565
566	error("Could not find path to: " + tool);
567	sys.exit(1);
568
569
570#####
571# ELF file comparison helper routines
572#
573
574#
575# Return a dictionary of ELF section types keyed by section name
576#
577def get_elfheader(f) :
578
579	header = {}
580
581	hstring = commands.getoutput(elfdump_cmd + " -c " + f)
582
583	if len(hstring) == 0 :
584		error("Failed to dump ELF header for " + f)
585		return
586
587	# elfdump(1) dumps the section headers with the section name
588	# following "sh_name:", and the section type following "sh_type:"
589	sections = hstring.split("Section Header")
590	for sect in sections :
591		datap = sect.find("sh_name:");
592		if datap == -1 :
593			continue
594		section = sect[datap:].split()[1]
595		datap = sect.find("sh_type:");
596		if datap == -1 :
597			error("Could not get type for sect: " + section + \
598			      " in " + f)
599		sh_type = sect[datap:].split()[2]
600		header[section] = sh_type
601
602	return header
603
604#
605# Extract data in the specified ELF section from the given file
606#
607def extract_elf_section(f, section) :
608
609	data = commands.getoutput(dump_cmd + " -sn " + section + " " + f)
610
611	if len(data) == 0 :
612		error(cmd + " yielded no data")
613		return
614
615	# dump(1) displays the file name to start...
616	# get past it to the data itself
617	dbegin = data.find(":") + 1
618	data = data[dbegin:];
619
620	return (data)
621
622#
623# Return a (hopefully meaningful) human readable set of diffs
624# for the specified ELF section between f1 and f2
625#
626# Depending on the section, various means for dumping and diffing
627# the data may be employed.
628#
629text_sections = [ '.text', '.init', '.fini' ]
630def diff_elf_section(f1, f2, section, sh_type) :
631
632	if (sh_type == "SHT_RELA") : # sh_type == SHT_RELA
633		cmd1 = elfdump_cmd + " -r " + f1 + " > " + tmpFile1
634		cmd2 = elfdump_cmd + " -r " + f2 + " > " + tmpFile2
635	elif (section == ".group") :
636		cmd1 = elfdump_cmd + " -g " + f1 + " > " + tmpFile1
637		cmd2 = elfdump_cmd + " -g " + f2 + " > " + tmpFile2
638	elif (section == ".hash") :
639		cmd1 = elfdump_cmd + " -h " + f1 + " > " + tmpFile1
640		cmd2 = elfdump_cmd + " -h " + f2 + " > " + tmpFile2
641	elif (section == ".dynamic") :
642		cmd1 = elfdump_cmd + " -d " + f1 + " > " + tmpFile1
643		cmd2 = elfdump_cmd + " -d " + f2 + " > " + tmpFile2
644	elif (section == ".got") :
645		cmd1 = elfdump_cmd + " -G " + f1 + " > " + tmpFile1
646		cmd2 = elfdump_cmd + " -G " + f2 + " > " + tmpFile2
647	elif (section == ".SUNW_cap") :
648		cmd1 = elfdump_cmd + " -H " + f1 + " > " + tmpFile1
649		cmd2 = elfdump_cmd + " -H " + f2 + " > " + tmpFile2
650	elif (section == ".interp") :
651		cmd1 = elfdump_cmd + " -i " + f1 + " > " + tmpFile1
652		cmd2 = elfdump_cmd + " -i " + f2 + " > " + tmpFile2
653	elif (section == ".symtab" or section == ".dynsym") :
654		cmd1 = elfdump_cmd + " -s -N " + section + " " + f1 + " > " + tmpFile1
655		cmd2 = elfdump_cmd + " -s -N " + section + " " + f2 + " > " + tmpFile2
656	elif (section in text_sections) :
657		# dis sometimes complains when it hits something it doesn't
658		# know how to disassemble. Just ignore it, as the output
659		# being generated here is human readable, and we've already
660		# correctly flagged the difference.
661		cmd1 = dis_cmd + " -t " + section + " " + f1 + \
662		       " 2>/dev/null | grep -v disassembly > " + tmpFile1
663		cmd2 = dis_cmd + " -t " + section + " " + f2 + \
664		       " 2>/dev/null | grep -v disassembly > " + tmpFile2
665	else :
666		cmd1 = elfdump_cmd + " -w " + tmpFile1 + " -N " + \
667		       section + " " + f1
668		cmd2 = elfdump_cmd + " -w " + tmpFile2 + " -N " + \
669		       section + " " + f2
670
671	os.system(cmd1)
672	os.system(cmd2)
673
674	data = diffFileData(tmpFile1, tmpFile2)
675
676	return (data)
677
678#
679# compare the relevant sections of two ELF binaries
680# and report any differences
681#
682# Returns: 1 if any differenes found
683#          0 if no differences found
684#	  -1 on error
685#
686
687# Sections deliberately not considered when comparing two ELF
688# binaries. Differences observed in these sections are not considered
689# significant where patch deliverable identification is concerned.
690sections_to_skip = [ ".SUNW_signature",
691		     ".comment",
692		     ".SUNW_ctf",
693		     ".debug",
694		     ".plt",
695		     ".rela.bss",
696		     ".rela.plt",
697		     ".line",
698		     ".note",
699		     ".compcom",
700		     ]
701
702sections_preferred = [ ".rodata.str1.8",
703		       ".rodata.str1.1",
704		       ".rodata",
705		       ".data1",
706		       ".data",
707		       ".text",
708		       ]
709
710def compareElfs(base, ptch, quiet) :
711
712	global logging
713
714	base_header = get_elfheader(base)
715 	sections = base_header.keys()
716
717	ptch_header = get_elfheader(ptch)
718	e2_only_sections = ptch_header.keys()
719
720	e1_only_sections = []
721
722	fileName = fnFormat(base)
723
724	# Derive the list of ELF sections found only in
725	# either e1 or e2.
726	for sect in sections :
727		if not sect in e2_only_sections :
728			e1_only_sections.append(sect)
729		else :
730			e2_only_sections.remove(sect)
731
732	if len(e1_only_sections) > 0 :
733		if quiet :
734			return 1
735		info(fileName);
736		if not logging :
737			return 1
738
739		slist = ""
740		for sect in e1_only_sections :
741			slist = slist + sect + "\t"
742		v_info("\nELF sections found in " + \
743		      base + " but not in " + ptch)
744		v_info("\n" + slist)
745		return 1
746
747	if len(e2_only_sections) > 0 :
748		if quiet :
749			return 1
750
751		info(fileName);
752		if not logging :
753			return 1
754
755		slist = ""
756		for sect in e2_only_sections :
757			slist = slist + sect + "\t"
758		v_info("\nELF sections found in " + \
759		      ptch + " but not in " + base)
760		v_info("\n" + slist)
761		return 1
762
763	# Look for preferred sections, and put those at the
764	# top of the list of sections to compare
765	for psect in sections_preferred :
766		if psect in sections :
767			sections.remove(psect)
768			sections.insert(0, psect)
769
770	# Compare ELF sections
771	first_section = True
772	for sect in sections :
773
774		if sect in sections_to_skip :
775			continue
776
777		s1 = extract_elf_section(base, sect);
778		s2 = extract_elf_section(ptch, sect);
779
780		if len(s1) != len (s2) or s1 != s2:
781			if not quiet:
782				sh_type = base_header[sect]
783				data = diff_elf_section(base, ptch, sect, \
784							sh_type)
785
786				# If all ELF sections are being reported, then
787				# invoke difference() to flag the file name to
788				# stdout only once. Any other section differences
789				# should be logged to the results file directly
790				if not first_section :
791					log_difference(fileName, "ELF " + sect, data)
792				else :
793					difference(fileName, "ELF " + sect, data)
794
795			if not reportAllSects :
796				return 1
797			first_section = False
798	return 0
799
800#####
801# Archive object comparison
802#
803# Returns 1 if difference detected
804#         0 if no difference detected
805#        -1 on error
806#
807def compareArchives(base, ptch, fileType) :
808
809	fileName = fnFormat(base)
810
811	# clear the temp directories
812	baseCmd = "rm -rf " + tmpDir1 + "*"
813	status, output = commands.getstatusoutput(baseCmd)
814	if status != 0 :
815		error(baseCmd + " failed: " + output)
816		return -1
817
818	ptchCmd = "rm -rf " + tmpDir2 + "*"
819	status, output = commands.getstatusoutput(ptchCmd)
820	if status != 0 :
821		error(ptchCmd + " failed: " + output)
822		return -1
823
824	#
825	# Be optimistic and first try a straight file compare
826	# as it will allow us to finish up quickly.
827	if compareBasic(base, ptch, True, fileType) == 0 :
828		return 0
829
830	# copy over the objects to the temp areas, and
831	# unpack them
832	baseCmd = "cp -fp " + base + " " + tmpDir1
833	status, output = commands.getstatusoutput(baseCmd)
834	if status != 0 :
835		error(baseCmd + " failed: " + output)
836		return -1
837
838	ptchCmd = "cp -fp " + ptch + " " + tmpDir2
839	status, output = commands.getstatusoutput(ptchCmd)
840	if status != 0 :
841		error(ptchCmd + " failed: " + output)
842		return -1
843
844	bname = string.split(fileName, '/')[-1]
845	if fileType == "Java Archive" :
846		baseCmd = "cd " + tmpDir1 + "; " + "jar xf " + bname + \
847			  "; rm -f " + bname + " META-INF/MANIFEST.MF"
848		ptchCmd = "cd " + tmpDir2 + "; " + "jar xf " + bname + \
849			  "; rm -f " + bname + " META-INF/MANIFEST.MF"
850	elif fileType == "ELF Object Archive" :
851		baseCmd = "cd " + tmpDir1 + "; " + "/usr/ccs/bin/ar x " + \
852			  bname + "; rm -f " + bname
853		ptchCmd = "cd " + tmpDir2 + "; " + "/usr/ccs/bin/ar x " + \
854			  bname + "; rm -f " + bname
855	else :
856		error("unexpected file type: " + fileType)
857		return -1
858
859	os.system(baseCmd)
860	os.system(ptchCmd)
861
862	baseFlist = list(findFiles(tmpDir1))
863	ptchFlist = list(findFiles(tmpDir2))
864
865	# Trim leading path off base/ptch file lists
866	flist = []
867	for fn in baseFlist :
868		flist.append(str_prefix_trunc(fn, tmpDir1))
869	baseFlist = flist
870
871	flist = []
872	for fn in ptchFlist :
873		flist.append(str_prefix_trunc(fn, tmpDir2))
874	ptchFlist = flist
875
876	for fn in ptchFlist :
877		if not fn in baseFlist :
878			difference(fileName, fileType, \
879				   fn + " added to " + fileName)
880			return 1
881
882	for fn in baseFlist :
883		if not fn in ptchFlist :
884			difference(fileName, fileType, \
885				   fn + " removed from " + fileName)
886			return 1
887
888		differs = compareOneFile((tmpDir1 + fn), (tmpDir2 + fn), True)
889		if differs :
890			difference(fileName, fileType, \
891				   fn + " in " + fileName + " differs")
892			return 1
893	return 0
894
895#####
896# (Basic) file comparison
897#
898# There's some special case code here for Javadoc HTML files
899#
900# Returns 1 if difference detected
901#         0 if no difference detected
902#        -1 on error
903#
904def compareBasic(base, ptch, quiet, fileType) :
905
906	fileName = fnFormat(base);
907
908	if quiet and os.stat(base)[ST_SIZE] != os.stat(ptch)[ST_SIZE] :
909		return 1
910
911	try:
912		baseFile = open(base)
913	except:
914		error("could not open " + base)
915		return -1
916	try:
917		ptchFile = open(ptch)
918	except:
919		error("could not open " + ptch)
920		return -1
921
922	baseData = baseFile.read()
923	ptchData = ptchFile.read()
924
925	baseFile.close()
926	ptchFile.close()
927
928	needToSnip = False
929	if fileType == "HTML" :
930		needToSnip = True
931		toSnipBeginStr = "<!-- Generated by javadoc"
932		toSnipEndStr = "-->\n"
933
934	if needToSnip :
935		toSnipBegin = string.find(baseData, toSnipBeginStr)
936		if toSnipBegin != -1 :
937			toSnipEnd = string.find(baseData[toSnipBegin:], \
938						toSnipEndStr) + \
939						len(toSnipEndStr)
940			baseData = baseData[:toSnipBegin] + \
941				   baseData[toSnipBegin + toSnipEnd:]
942			ptchData = ptchData[:toSnipBegin] + \
943				   ptchData[toSnipBegin + toSnipEnd:]
944
945	if quiet :
946		if baseData != ptchData :
947			return 1
948	else :
949		if len(baseData) != len(ptchData) or baseData != ptchData :
950			diffs = diffData(baseData, ptchData)
951			difference(fileName, fileType, diffs)
952			return 1
953	return 0
954
955
956#####
957# Compare two objects by producing a data dump from
958# each object, and then comparing the dump data
959#
960# Returns: 1 if a difference is detected
961#          0 if no difference detected
962#         -1 upon error
963#
964def compareByDumping(base, ptch, quiet, fileType) :
965
966	fileName = fnFormat(base);
967
968	if fileType == "Lint Library" :
969		baseCmd = lintdump_cmd + " -ir " + base + \
970			  " | egrep -v '(LINTOBJ|LINTMOD):'" + " > " + tmpFile1
971		ptchCmd = lintdump_cmd + " -ir " + ptch + \
972			  " | egrep -v '(LINTOBJ|LINTMOD):'" + " > " + tmpFile2
973	elif fileType == "Sqlite Database" :
974		baseCmd = "echo .dump | " + sqlite_cmd + base + " > " + \
975			  tmpFile1
976		ptchCmd = "echo .dump | " + sqlite_cmd + ptch + " > " + \
977			  tmpFile2
978
979	os.system(baseCmd)
980	os.system(ptchCmd)
981
982	try:
983		baseFile = open(tmpFile1)
984	except:
985		error("could not open: " + tmpFile1)
986	try:
987		ptchFile = open(tmpFile2)
988	except:
989		error("could not open: " + tmpFile2)
990
991	baseData = baseFile.read()
992	ptchData = ptchFile.read()
993
994	baseFile.close()
995	ptchFile.close()
996
997	if len(baseData) != len(ptchData) or baseData != ptchData :
998		if not quiet :
999			data = diffFileData(tmpFile1, tmpFile2);
1000			difference(fileName, fileType, data)
1001 		return 1
1002	return 0
1003
1004#####
1005# Compare two elfsign activation files. This ignores the activation
1006# files themselves and reports a difference if and only if the
1007# corresponding base files are different.
1008#
1009# Returns 1 if difference detected
1010#         0 if no difference detected
1011#        -1 on error
1012#
1013def compareActivation(base, ptch, quiet, fileType) :
1014
1015	fileName = fnFormat(base)
1016
1017	# Drop the .esa suffix from both filenames.
1018	base = base[0:base.rfind('.esa')]
1019	ptch = ptch[0:ptch.rfind('.esa')]
1020
1021	result = compareOneFile(base, ptch, True)
1022	if result == -1 :
1023		error("unable to compare " + fileName)
1024	elif result == 1 :
1025		if not quiet :
1026			difference(fileName, fileType, \
1027				"change in corresponding ELF file")
1028
1029	return result
1030
1031#####
1032# Compare two objects. Detect type changes.
1033# Vector off to the appropriate type specific
1034# compare routine based on the type.
1035#
1036def compareOneFile(base, ptch, quiet) :
1037
1038	# Verify the file types.
1039	# If they are different, indicate this and move on
1040	btype = getTheFileType(base)
1041	ptype = getTheFileType(ptch)
1042
1043	if btype == 'Error' or ptype == 'Error' :
1044		return -1
1045
1046	fileName = fnFormat(base)
1047
1048	if (btype != ptype) :
1049		if not quiet :
1050			difference(fileName, "file type", btype + " to " + ptype)
1051		return 1
1052	else :
1053		fileType = btype
1054
1055	if (fileType == 'ELF') :
1056		return compareElfs(base, ptch, quiet)
1057
1058	elif (fileType == 'Java Archive' or fileType == 'ELF Object Archive') :
1059		return compareArchives(base, ptch, fileType)
1060
1061	elif (fileType == 'HTML') :
1062		return compareBasic(base, ptch, quiet, fileType)
1063
1064	elif ( fileType == 'Lint Library' ) :
1065		return compareByDumping(base, ptch, quiet, fileType)
1066
1067	elif ( fileType == 'Sqlite Database' ) :
1068		return compareByDumping(base, ptch, quiet, fileType)
1069
1070	elif ( fileType == 'Elfsign Activation' ) :
1071		return compareActivation(base, ptch, quiet, fileType)
1072
1073	else :
1074		# it has to be some variety of text file
1075		return compareBasic(base, ptch, quiet, fileType)
1076
1077# Cleanup and self-terminate
1078def cleanup(ret) :
1079
1080	if len(tmpDir1) > 0 and len(tmpDir2) > 0 :
1081
1082		baseCmd = "rm -rf " + tmpDir1
1083		ptchCmd = "rm -rf " + tmpDir2
1084
1085		os.system(baseCmd)
1086		os.system(ptchCmd)
1087
1088	if logging :
1089		log.close()
1090
1091	sys.exit(ret)
1092
1093def main() :
1094
1095	# Log file handle
1096	global log
1097
1098	# Globals relating to command line options
1099	global logging, vdiffs, reportAllSects
1100
1101	# Named temporary files / directories
1102	global tmpDir1, tmpDir2, tmpFile1, tmpFile2
1103
1104	# Command paths
1105	global lintdump_cmd, elfdump_cmd, dump_cmd, dis_cmd, od_cmd, diff_cmd, sqlite_cmd
1106
1107	# Default search path
1108	global wsdiff_path
1109
1110	# Essentially "uname -p"
1111	global arch
1112
1113	# Some globals need to be initialized
1114	logging = vdiffs = reportAllSects = False
1115
1116
1117	# Process command line arguments
1118	# Return values are returned from args() in alpha order
1119	# (Yes, python functions can return multiple values (ewww))
1120	# Note that args() also set the globals:
1121	#	logging to True if verbose logging (to a file) was enabled
1122	#	vdiffs to True if logged differences aren't to be truncated
1123	#	reportAllSects to True if all ELF section differences are to be reported
1124	#
1125	baseRoot, fileNamesFile, localTools, ptchRoot, results = args()
1126
1127	#
1128	# Set up the results/log file
1129	#
1130	if logging :
1131		try:
1132			log = open(results, "w")
1133		except:
1134			logging = False
1135			error("failed to open log file: " + log)
1136			sys.exit(1)
1137
1138		dateTimeStr= "# %d/%d/%d at %d:%d:%d" % time.localtime()[:6]
1139		v_info("# This file was produced by wsdiff")
1140		v_info(dateTimeStr)
1141
1142	#
1143	# Build paths to the tools required tools
1144	#
1145	# Try to look for tools in $SRC/tools if the "-t" option
1146	# was specified
1147	#
1148	arch = commands.getoutput("uname -p")
1149	if localTools :
1150		try:
1151			src = os.environ['SRC']
1152		except:
1153			error("-t specified, but $SRC not set. Cannot find $SRC/tools")
1154			src = ""
1155		if len(src) > 0 :
1156			wsdiff_path.insert(0, src + "/tools/proto/opt/onbld/bin")
1157
1158	lintdump_cmd = find_tool("lintdump")
1159	elfdump_cmd = find_tool("elfdump")
1160	dump_cmd = find_tool("dump")
1161	od_cmd = find_tool("od")
1162	dis_cmd = find_tool("dis")
1163	diff_cmd = find_tool("diff")
1164	sqlite_cmd = find_tool("sqlite")
1165
1166	#
1167	# validate the base and patch paths
1168	#
1169	if baseRoot[-1] != '/' :
1170		baseRoot += '/'
1171
1172	if ptchRoot[-1] != '/' :
1173		ptchRoot += '/'
1174
1175	if not os.path.exists(baseRoot) :
1176		error("old proto area: " + baseRoot + " does not exist")
1177		sys.exit(1)
1178
1179	if not os.path.exists(ptchRoot) :
1180		error("new proto area: " + ptchRoot + \
1181		      " does not exist")
1182		sys.exit(1)
1183
1184	#
1185	# log some information identifying the run
1186	#
1187	v_info("Old proto area: " + baseRoot)
1188	v_info("New proto area: " + ptchRoot)
1189	v_info("Results file: " + results + "\n")
1190
1191	#
1192	# Set up the temporary directories / files
1193	# Could use python's tmpdir routines, but these should
1194	# be easier to identify / keep around for debugging
1195	pid = os.getpid()
1196	tmpDir1 = "/tmp/wsdiff_tmp1_" + str(pid) + "/"
1197	tmpDir2 = "/tmp/wsdiff_tmp2_" + str(pid) + "/"
1198	if not os.path.exists(tmpDir1) :
1199		os.makedirs(tmpDir1)
1200	if not os.path.exists(tmpDir2) :
1201		os.makedirs(tmpDir2)
1202
1203	tmpFile1 = tmpDir1 + "f1"
1204	tmpFile2 = tmpDir2 + "f2"
1205
1206	# Derive a catalog of new, deleted, and to-be-compared objects
1207	# either from the specified base and patch proto areas, or from
1208	# from an input file list
1209	newOrDeleted = False
1210
1211	if fileNamesFile != "" :
1212		changedFiles, newFiles, deletedFiles = \
1213			      flistCatalog(baseRoot, ptchRoot, fileNamesFile)
1214	else :
1215		changedFiles, newFiles, deletedFiles = protoCatalog(baseRoot, ptchRoot)
1216
1217	if len(newFiles) > 0 :
1218		newOrDeleted = True
1219		info("\nNew objects found: ")
1220
1221		for fn in newFiles :
1222			info(fnFormat(fn))
1223
1224	if len(deletedFiles) > 0 :
1225		newOrDeleted = True
1226		info("\nObjects removed: ")
1227
1228		for fn in deletedFiles :
1229			info(fnFormat(fn))
1230
1231	if newOrDeleted :
1232		info("\nChanged objects: ");
1233
1234
1235	# Here's where all the heavy lifting happens
1236	# Perform a comparison on each object appearing in
1237	# both proto areas. compareOneFile will examine the
1238	# file types of each object, and will vector off to
1239	# the appropriate comparison routine, where the compare
1240	# will happen, and any differences will be reported / logged
1241	for fn in changedFiles :
1242		base = baseRoot + fn
1243		ptch = ptchRoot + fn
1244
1245		compareOneFile(base, ptch, False)
1246
1247	# We're done, cleanup.
1248	cleanup(0)
1249
1250if __name__ == '__main__' :
1251	try:
1252		main()
1253	except KeyboardInterrupt :
1254		cleanup(1);
1255
1256
1257