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