xref: /illumos-gate/usr/src/tools/scripts/wsdiff.py (revision 632cbd96e544a0a21f1a1f7ca071b145f379215d)
1#!@TOOLS_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 (c) 2010, Oracle and/or its affiliates. All rights reserved.
23# Copyright 2018 OmniOS Community Edition (OmniOSce) Association.
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
73from __future__ import print_function
74import datetime, fnmatch, getopt, os, profile, io, subprocess
75import re, resource, select, shutil, signal, string, struct, sys, tempfile
76import time, threading
77from stat import *
78from subprocess import Popen, PIPE
79
80# Human readable diffs truncated by default if longer than this
81# Specifying -v on the command line will override
82diffs_sz_thresh = 4096
83
84# Lock name	 Provides exclusive access to
85# --------------+------------------------------------------------
86# output_lock	 standard output or temporary file (difference())
87# log_lock	 the results file (log_difference())
88# wset_lock	 changedFiles list (workerThread())
89output_lock = threading.Lock()
90log_lock = threading.Lock()
91wset_lock = threading.Lock()
92
93# Variable for thread control
94keep_processing = True
95
96# Default search path for wsdiff
97wsdiff_path = [ "/usr/bin",
98		"/usr/ccs/bin",
99		"/lib/svc/bin",
100		"/opt/onbld/bin" ]
101
102# These are objects that wsdiff will notice look different, but will not report.
103# Existence of an exceptions list, and adding things here is *dangerous*,
104# and therefore the *only* reasons why anything would be listed here is because
105# the objects do not build deterministically, yet we *cannot* fix this.
106#
107# These perl libraries use __DATE__ and therefore always look different.
108# Ideally, we would purge use the use of __DATE__ from the source, but because
109# this is source we wish to distribute with Solaris "unchanged", we cannot modify.
110#
111wsdiff_exceptions = [
112	"usr/perl5/5.8.4/lib/sun4-solaris-64int/CORE/libperl.so.1",
113	"usr/perl5/5.6.1/lib/sun4-solaris-64int/CORE/libperl.so.1",
114	"usr/perl5/5.8.4/lib/i86pc-solaris-64int/CORE/libperl.so.1",
115	"usr/perl5/5.6.1/lib/i86pc-solaris-64int/CORE/libperl.so.1"
116]
117
118def getoutput(cmd):
119	p = Popen(cmd, shell=True, stdout=PIPE)
120	output, x = p.communicate()
121	return (p.returncode, output.decode(errors='replace'))
122
123#####
124# Logging routines
125#
126
127# Debug message to be printed to the screen, and the log file
128def debug(msg) :
129
130	# Add prefix to highlight debugging message
131	msg = "## " + msg
132	if debugon :
133		output_lock.acquire()
134		print(msg)
135		sys.stdout.flush()
136		output_lock.release()
137		if logging :
138			log_lock.acquire()
139			print(msg, file=log)
140			log.flush()
141			log_lock.release()
142
143# Informational message to be printed to the screen, and the log file
144def info(msg) :
145
146	output_lock.acquire()
147	print(msg)
148	sys.stdout.flush()
149	output_lock.release()
150	if logging :
151		log_lock.acquire()
152		print(msg, file=log)
153		log.flush()
154		log_lock.release()
155
156# Error message to be printed to the screen, and the log file
157def error(msg) :
158
159	output_lock.acquire()
160	print("ERROR: " + msg, file=sys.stderr)
161	sys.stderr.flush()
162	output_lock.release()
163	if logging :
164		log_lock.acquire()
165		print("ERROR: " + msg, file=log)
166		log.flush()
167		log_lock.release()
168
169# Informational message to be printed only to the log, if there is one.
170def v_info(msg) :
171
172	if logging :
173		log_lock.acquire()
174		print(msg, file=log)
175		log.flush()
176		log_lock.release()
177
178#
179# Flag a detected file difference
180# Display the fileName to stdout, and log the difference
181#
182def difference(f, dtype, diffs) :
183
184	if f in wsdiff_exceptions :
185		return
186
187	output_lock.acquire()
188	if sorted :
189		differentFiles.append(f)
190	else:
191		print(f)
192		sys.stdout.flush()
193	output_lock.release()
194
195	log_difference(f, dtype, diffs)
196
197#
198# Do the actual logging of the difference to the results file
199#
200def log_difference(f, dtype, diffs) :
201
202	if logging :
203		log_lock.acquire()
204		print(f, file=log)
205		print("NOTE: " + dtype + " difference detected.", file=log)
206
207		difflen = len(diffs)
208		if difflen > 0 :
209			print('', file=log)
210
211			if not vdiffs and difflen > diffs_sz_thresh :
212				print(diffs[:diffs_sz_thresh], file=log)
213				print("... truncated due to length: " +
214				      "use -v to override ...", file=log)
215			else :
216				print(diffs, file=log)
217			print('\n', file=log)
218		log.flush()
219		log_lock.release()
220
221
222#####
223# diff generating routines
224#
225
226#
227# Return human readable diffs from two temporary files
228#
229def diffFileData(tmpf1, tmpf2) :
230
231	binaries = False
232
233	# Filter the data through od(1) if the data is detected
234	# as being binary
235	if isBinary(tmpf1) or isBinary(tmpf2) :
236		binaries = True
237		tmp_od1 = tmpf1 + ".od"
238		tmp_od2 = tmpf2 + ".od"
239
240		cmd = od_cmd + " -c -t x4" + " " + tmpf1 + " > " + tmp_od1
241		os.system(cmd)
242		cmd = od_cmd + " -c -t x4" + " " + tmpf2 + " > " + tmp_od2
243		os.system(cmd)
244
245		tmpf1 = tmp_od1
246		tmpf2 = tmp_od2
247
248	try:
249		rc, data = getoutput(diff_cmd + " " + tmpf1 + " " + tmpf2)
250		# Remove the temp files as we no longer need them.
251		if binaries :
252			try:
253				os.unlink(tmp_od1)
254			except OSError as e:
255				error("diffFileData: unlink failed %s" % e)
256			try:
257				os.unlink(tmp_od2)
258			except OSError as e:
259				error("diffFileData: unlink failed %s" % e)
260	except:
261		error("failed to get output of command: " + diff_cmd + " "
262		    + tmpf1 + " " + tmpf2)
263
264		# Send exception for the failed command up
265		raise
266		return
267
268	return data
269
270#
271# Return human readable diffs betweeen two datasets
272#
273def diffData(base, ptch, d1, d2) :
274
275	t = threading.currentThread()
276	tmpFile1 = tmpDir1 + os.path.basename(base) + t.getName()
277	tmpFile2 = tmpDir2 + os.path.basename(ptch) + t.getName()
278
279	try:
280		fd1 = io.open(tmpFile1, mode='w', errors='ignore')
281	except:
282		error("failed to open: " + tmpFile1)
283		cleanup(1)
284
285	try:
286		fd2 = io.open(tmpFile2, mode='w', errors='ignore')
287	except:
288		error("failed to open: " + tmpFile2)
289		cleanup(1)
290
291	fd1.write(d1)
292	fd2.write(d2)
293	fd1.close()
294	fd2.close()
295
296	return diffFileData(tmpFile1, tmpFile2)
297
298#####
299# Misc utility functions
300#
301
302# Prune off the leading prefix from string s
303def str_prefix_trunc(s, prefix) :
304	snipLen = len(prefix)
305	return s[snipLen:]
306
307#
308# Prune off leading proto path goo (if there is one) to yield
309# the deliverable's eventual path relative to root
310# e.g. proto.base/root_sparc/usr/src/cmd/prstat => usr/src/cmd/prstat
311#
312def fnFormat(fn) :
313	root_arch_str = "root_" + arch
314
315	pos = fn.find(root_arch_str)
316	if pos == -1 :
317		return fn
318
319	pos = fn.find("/", pos)
320	if pos == -1 :
321		return fn
322
323	return fn[pos + 1:]
324
325#####
326# Usage / argument processing
327#
328
329#
330# Display usage message
331#
332def usage() :
333	sys.stdout.flush()
334	print("""Usage: wsdiff [-dvVst] [-r results ] [-i filelist ] old new
335        -d      Print debug messages about the progress
336        -v      Do not truncate observed diffs in results
337        -V      Log *all* ELF sect diffs vs. logging the first diff found
338        -t      Use onbld tools in $SRC/tools
339        -r      Log results and observed differences
340        -s      Produce sorted list of differences
341        -i      Tell wsdiff which objects to compare via an input file list""",
342	    file=sys.stderr)
343	sys.exit(1)
344
345#
346# Process command line options
347#
348def args() :
349
350	global debugon
351	global logging
352	global vdiffs
353	global reportAllSects
354	global sorted
355
356	validOpts = 'di:r:vVst?'
357
358	baseRoot = ""
359	ptchRoot = ""
360	fileNamesFile = ""
361	results = ""
362	localTools = False
363
364	# getopt.getopt() returns:
365	#	an option/value tuple
366	#	a list of remaining non-option arguments
367	#
368	# A correct wsdiff invocation will have exactly two non option
369	# arguments, the paths to the base (old), ptch (new) proto areas
370	try:
371		optlist, args = getopt.getopt(sys.argv[1:], validOpts)
372	except getopt.error as val:
373		usage()
374
375	if len(args) != 2 :
376		usage();
377
378	for opt,val in optlist :
379		if opt == '-d' :
380			debugon = True
381		elif opt == '-i' :
382			fileNamesFile = val
383		elif opt == '-r' :
384			results = val
385			logging = True
386		elif opt == '-s' :
387			sorted = True
388		elif opt == '-v' :
389			vdiffs = True
390		elif opt == '-V' :
391			reportAllSects = True
392		elif opt == '-t':
393			localTools = True
394		else:
395			usage()
396
397	baseRoot = args[0]
398	ptchRoot = args[1]
399
400	if len(baseRoot) == 0 or len(ptchRoot) == 0 :
401		usage()
402
403	if logging and len(results) == 0 :
404		usage()
405
406	if vdiffs and not logging :
407		error("The -v option requires a results file (-r)")
408		sys.exit(1)
409
410	if reportAllSects and not logging :
411		error("The -V option requires a results file (-r)")
412		sys.exit(1)
413
414	# alphabetical order
415	return	baseRoot, fileNamesFile, localTools, ptchRoot, results
416
417#####
418# File identification
419#
420
421#
422# Identify the file type.
423# If it's not ELF, use the file extension to identify
424# certain file types that require special handling to
425# compare. Otherwise just return a basic "ASCII" type.
426#
427def getTheFileType(f) :
428
429	extensions = { 'a'	:	'ELF Object Archive',
430		       'jar'	:	'Java Archive',
431		       'html'	:	'HTML',
432		       'ln'	:	'Lint Library',
433		       'db'	:	'Sqlite Database' }
434
435	try:
436		if os.stat(f)[ST_SIZE] == 0 :
437			return 'ASCII'
438	except:
439		error("failed to stat " + f)
440		return 'Error'
441
442	if isELF(f) == 1 :
443		return 'ELF'
444
445	fnamelist = f.split('.')
446	if len(fnamelist) > 1 :	# Test the file extension
447		extension = fnamelist[-1]
448		if extension in extensions.keys():
449			return extensions[extension]
450
451	return 'ASCII'
452
453#
454# Return non-zero if "f" is an ELF file
455#
456elfmagic = b'\177ELF'
457def isELF(f) :
458	try:
459		with io.open(f, mode='rb') as fd:
460			magic = fd.read(len(elfmagic))
461
462		if magic == elfmagic :
463			return 1
464	except:
465		pass
466	return 0
467
468#
469# Return non-zero is "f" is binary.
470# Consider the file to be binary if it contains any null characters
471#
472def isBinary(f) :
473	try:
474		with io.open(f, mode='rb') as fd:
475			s = fd.read()
476
477		if s.find(b'\0') == -1 :
478			return 0
479	except:
480		pass
481	return 1
482
483#####
484# Directory traversal and file finding
485#
486
487#
488# Return a sorted list of files found under the specified directory
489#
490def findFiles(d) :
491	for path, subdirs, files in os.walk(d) :
492		files.sort()
493		for name in files :
494			yield os.path.join(path, name)
495
496#
497# Examine all files in base, ptch
498#
499# Return a list of files appearing in both proto areas,
500# a list of new files (files found only in ptch) and
501# a list of deleted files (files found only in base)
502#
503def protoCatalog(base, ptch) :
504
505	compFiles = []		# List of files in both proto areas
506	ptchList = []		# List of file in patch proto area
507
508	newFiles = []		# New files detected
509	deletedFiles = []	# Deleted files
510
511	debug("Getting the list of files in the base area");
512	baseFilesList = list(findFiles(base))
513	baseStringLength = len(base)
514	debug("Found " + str(len(baseFilesList)) + " files")
515
516	debug("Getting the list of files in the patch area");
517	ptchFilesList = list(findFiles(ptch))
518	ptchStringLength = len(ptch)
519	debug("Found " + str(len(ptchFilesList)) + " files")
520
521	# Inventory files in the base proto area
522	debug("Determining the list of regular files in the base area");
523	for fn in baseFilesList :
524		if os.path.islink(fn) :
525			continue
526
527		fileName = fn[baseStringLength:]
528		compFiles.append(fileName)
529	debug("Found " + str(len(compFiles)) + " files")
530
531	# Inventory files in the patch proto area
532	debug("Determining the list of regular files in the patch area");
533	for fn in ptchFilesList :
534		if os.path.islink(fn) :
535			continue
536
537		fileName = fn[ptchStringLength:]
538		ptchList.append(fileName)
539	debug("Found " + str(len(ptchList)) + " files")
540
541	# Deleted files appear in the base area, but not the patch area
542	debug("Searching for deleted files by comparing the lists")
543	for fileName in compFiles :
544		if not fileName in ptchList :
545			deletedFiles.append(fileName)
546	debug("Found " + str(len(deletedFiles)) + " deleted files")
547
548	# Eliminate "deleted" files from the list of objects appearing
549	# in both the base and patch proto areas
550	debug("Eliminating deleted files from the list of objects")
551	for fileName in deletedFiles :
552		try:
553			compFiles.remove(fileName)
554		except:
555			error("filelist.remove() failed")
556	debug("List for comparison reduced to " + str(len(compFiles))
557	    + " files")
558
559	# New files appear in the patch area, but not the base
560	debug("Getting the list of newly added files")
561	for fileName in ptchList :
562		if not fileName in compFiles :
563			newFiles.append(fileName)
564	debug("Found " + str(len(newFiles)) + " new files")
565
566	return compFiles, newFiles, deletedFiles
567
568#
569# Examine the files listed in the input file list
570#
571# Return a list of files appearing in both proto areas,
572# a list of new files (files found only in ptch) and
573# a list of deleted files (files found only in base)
574#
575def flistCatalog(base, ptch, flist) :
576	compFiles = []		# List of files in both proto areas
577	newFiles = []		# New files detected
578	deletedFiles = []	# Deleted files
579
580	try:
581		fd = open(flist, "r")
582	except:
583		error("could not open: " + flist)
584		cleanup(1)
585
586	files = []
587	files = fd.readlines()
588	fd.close()
589
590	for f in files :
591		ptch_present = True
592		base_present = True
593
594		if f == '\n' :
595			continue
596
597		# the fileNames have a trailing '\n'
598		f = f.rstrip()
599
600		# The objects in the file list have paths relative
601		# to $ROOT or to the base/ptch directory specified on
602		# the command line.
603		# If it's relative to $ROOT, we'll need to add back the
604		# root_`uname -p` goo we stripped off in fnFormat()
605		if os.path.exists(base + f) :
606			fn = f;
607		elif os.path.exists(base + "root_" + arch + "/" + f) :
608			fn = "root_" + arch + "/" + f
609		else :
610			base_present = False
611
612		if base_present :
613			if not os.path.exists(ptch + fn) :
614				ptch_present = False
615		else :
616			if os.path.exists(ptch + f) :
617				fn = f
618			elif os.path.exists(ptch + "root_" + arch + "/" + f) :
619				fn = "root_" + arch + "/" + f
620			else :
621				ptch_present = False
622
623		if os.path.islink(base + fn) :	# ignore links
624			base_present = False
625		if os.path.islink(ptch + fn) :
626			ptch_present = False
627
628		if base_present and ptch_present :
629			compFiles.append(fn)
630		elif base_present :
631			deletedFiles.append(fn)
632		elif ptch_present :
633			newFiles.append(fn)
634		else :
635			if (os.path.islink(base + fn) and
636			    os.path.islink(ptch + fn)) :
637				continue
638			error(f + " in file list, but not in either tree. " +
639			    "Skipping...")
640
641	return compFiles, newFiles, deletedFiles
642
643
644#
645# Build a fully qualified path to an external tool/utility.
646# Consider the default system locations. For onbld tools, if
647# the -t option was specified, we'll try to use built tools in $SRC tools,
648# and otherwise, we'll fall back on /opt/onbld/
649#
650def find_tool(tool) :
651
652	# First, check what was passed
653	if os.path.exists(tool) :
654		return tool
655
656	# Next try in wsdiff path
657	for pdir in wsdiff_path :
658		location = pdir + "/" + tool
659		if os.path.exists(location) :
660			return location + " "
661
662		location = pdir + "/" + arch + "/" + tool
663		if os.path.exists(location) :
664			return location + " "
665
666	error("Could not find path to: " + tool);
667	sys.exit(1);
668
669
670#####
671# ELF file comparison helper routines
672#
673
674#
675# Return a dictionary of ELF section types keyed by section name
676#
677def get_elfheader(f) :
678
679	header = {}
680
681	rc, hstring = getoutput(elfdump_cmd + " -c " + f)
682
683	if len(hstring) == 0 :
684		error("Failed to dump ELF header for " + f)
685		raise
686		return
687
688	# elfdump(1) dumps the section headers with the section name
689	# following "sh_name:", and the section type following "sh_type:"
690	sections = hstring.split("Section Header")
691	for sect in sections :
692		datap = sect.find("sh_name:");
693		if datap == -1 :
694			continue
695		section = sect[datap:].split()[1]
696		datap = sect.find("sh_type:");
697		if datap == -1 :
698			error("Could not get type for sect: " + section +
699			      " in " + f)
700		sh_type = sect[datap:].split()[2]
701		header[section] = sh_type
702
703	return header
704
705#
706# Extract data in the specified ELF section from the given file
707#
708def extract_elf_section(f, section) :
709
710	rc, data = getoutput(dump_cmd + " -sn " + section + " " + f)
711
712	if len(data) == 0 :
713		error(dump_cmd + "yielded no data on section " + section +
714		    " of " + f)
715		raise
716		return
717
718	# dump(1) displays the file name to start...
719	# get past it to the data itself
720	dbegin = data.find(":") + 1
721	data = data[dbegin:];
722
723	return (data)
724
725#
726# Return a (hopefully meaningful) human readable set of diffs
727# for the specified ELF section between f1 and f2
728#
729# Depending on the section, various means for dumping and diffing
730# the data may be employed.
731#
732text_sections = [ '.text', '.init', '.fini' ]
733def diff_elf_section(f1, f2, section, sh_type) :
734
735	t = threading.currentThread()
736	tmpFile1 = tmpDir1 + os.path.basename(f1) + t.getName()
737	tmpFile2 = tmpDir2 + os.path.basename(f2) + t.getName()
738
739	if (sh_type == "SHT_RELA") : # sh_type == SHT_RELA
740		cmd1 = elfdump_cmd + " -r " + f1 + " > " + tmpFile1
741		cmd2 = elfdump_cmd + " -r " + f2 + " > " + tmpFile2
742	elif (section == ".group") :
743		cmd1 = elfdump_cmd + " -g " + f1 + " > " + tmpFile1
744		cmd2 = elfdump_cmd + " -g " + f2 + " > " + tmpFile2
745	elif (section == ".hash") :
746		cmd1 = elfdump_cmd + " -h " + f1 + " > " + tmpFile1
747		cmd2 = elfdump_cmd + " -h " + f2 + " > " + tmpFile2
748	elif (section == ".dynamic") :
749		cmd1 = elfdump_cmd + " -d " + f1 + " > " + tmpFile1
750		cmd2 = elfdump_cmd + " -d " + f2 + " > " + tmpFile2
751	elif (section == ".got") :
752		cmd1 = elfdump_cmd + " -G " + f1 + " > " + tmpFile1
753		cmd2 = elfdump_cmd + " -G " + f2 + " > " + tmpFile2
754	elif (section == ".SUNW_cap") :
755		cmd1 = elfdump_cmd + " -H " + f1 + " > " + tmpFile1
756		cmd2 = elfdump_cmd + " -H " + f2 + " > " + tmpFile2
757	elif (section == ".interp") :
758		cmd1 = elfdump_cmd + " -i " + f1 + " > " + tmpFile1
759		cmd2 = elfdump_cmd + " -i " + f2 + " > " + tmpFile2
760	elif (section == ".symtab" or section == ".dynsym") :
761		cmd1 = (elfdump_cmd + " -s -N " + section + " " + f1 +
762		    " > " + tmpFile1)
763		cmd2 = (elfdump_cmd + " -s -N " + section + " " + f2 +
764		    " > " + tmpFile2)
765	elif (section in text_sections) :
766		# dis sometimes complains when it hits something it doesn't
767		# know how to disassemble. Just ignore it, as the output
768		# being generated here is human readable, and we've already
769		# correctly flagged the difference.
770		cmd1 = (dis_cmd + " -t " + section + " " + f1 +
771		       " 2>/dev/null | grep -v disassembly > " + tmpFile1)
772		cmd2 = (dis_cmd + " -t " + section + " " + f2 +
773		       " 2>/dev/null | grep -v disassembly > " + tmpFile2)
774	else :
775		cmd1 = (elfdump_cmd + " -w " + tmpFile1 + " -N " +
776		       section + " " + f1)
777		cmd2 = (elfdump_cmd + " -w " + tmpFile2 + " -N " +
778		       section + " " + f2)
779
780	os.system(cmd1)
781	os.system(cmd2)
782
783	data = diffFileData(tmpFile1, tmpFile2)
784
785	# remove temp files as we no longer need them
786	try:
787		os.unlink(tmpFile1)
788	except OSError as e:
789		error("diff_elf_section: unlink failed %s" % e)
790	try:
791		os.unlink(tmpFile2)
792	except OSError as e:
793		error("diff_elf_section: unlink failed %s" % e)
794
795	return (data)
796
797#
798# compare the relevant sections of two ELF binaries
799# and report any differences
800#
801# Returns: 1 if any differenes found
802#          0 if no differences found
803#	  -1 on error
804#
805
806# Sections deliberately not considered when comparing two ELF
807# binaries. Differences observed in these sections are not considered
808# significant where patch deliverable identification is concerned.
809sections_to_skip = [ ".SUNW_signature",
810		     ".comment",
811		     ".SUNW_ctf",
812		     ".debug",
813		     ".plt",
814		     ".rela.bss",
815		     ".rela.plt",
816		     ".line",
817		     ".note",
818		     ".compcom",
819		     ]
820
821sections_preferred = [ ".rodata.str1.8",
822		       ".rodata.str1.1",
823		       ".rodata",
824		       ".data1",
825		       ".data",
826		       ".text",
827		       ]
828
829def compareElfs(base, ptch, quiet) :
830
831	global logging
832
833	try:
834		base_header = get_elfheader(base)
835	except:
836		return
837	sections = list(base_header.keys())
838
839	try:
840		ptch_header = get_elfheader(ptch)
841	except:
842		return
843	e2_only_sections = list(ptch_header.keys())
844
845	e1_only_sections = []
846
847	fileName = fnFormat(base)
848
849	# Derive the list of ELF sections found only in
850	# either e1 or e2.
851	for sect in sections :
852		if not sect in e2_only_sections :
853			e1_only_sections.append(sect)
854		else :
855			e2_only_sections.remove(sect)
856
857	if len(e1_only_sections) > 0 :
858		if quiet :
859			return 1
860
861		data = ""
862		if logging :
863			slist = ""
864			for sect in e1_only_sections :
865				slist = slist + sect + "\t"
866			data = ("ELF sections found in " +
867				base + " but not in " + ptch +
868				"\n\n" + slist)
869
870		difference(fileName, "ELF", data)
871		return 1
872
873	if len(e2_only_sections) > 0 :
874		if quiet :
875			return 1
876
877		data = ""
878		if logging :
879			slist = ""
880			for sect in e2_only_sections :
881				slist = slist + sect + "\t"
882			data = ("ELF sections found in " +
883				ptch + " but not in " + base +
884				"\n\n" + slist)
885
886		difference(fileName, "ELF", data)
887		return 1
888
889	# Look for preferred sections, and put those at the
890	# top of the list of sections to compare
891	for psect in sections_preferred :
892		if psect in sections :
893			sections.remove(psect)
894			sections.insert(0, psect)
895
896	# Compare ELF sections
897	first_section = True
898	for sect in sections :
899
900		if sect in sections_to_skip :
901			continue
902
903		try:
904			s1 = extract_elf_section(base, sect);
905		except:
906			return
907
908		try:
909			s2 = extract_elf_section(ptch, sect);
910		except:
911			return
912
913		if len(s1) != len (s2) or s1 != s2:
914			if not quiet:
915				sh_type = base_header[sect]
916				data = diff_elf_section(base, ptch,
917							sect, sh_type)
918
919				# If all ELF sections are being reported, then
920				# invoke difference() to flag the file name to
921				# stdout only once. Any other section differences
922				# should be logged to the results file directly
923				if not first_section :
924					log_difference(fileName,
925					    "ELF " + sect, data)
926				else :
927					difference(fileName, "ELF " + sect,
928					    data)
929
930			if not reportAllSects :
931				return 1
932			first_section = False
933
934	return 0
935
936#####
937# recursively remove 2 directories
938#
939# Used for removal of temporary directory strucures (ignores any errors).
940#
941def clearTmpDirs(dir1, dir2) :
942
943	if os.path.isdir(dir1) > 0 :
944		shutil.rmtree(dir1, True)
945
946	if os.path.isdir(dir2) > 0 :
947		shutil.rmtree(dir2, True)
948
949
950#####
951# Archive object comparison
952#
953# Returns 1 if difference detected
954#         0 if no difference detected
955#        -1 on error
956#
957def compareArchives(base, ptch, fileType) :
958
959	fileName = fnFormat(base)
960	t = threading.currentThread()
961	ArchTmpDir1 = tmpDir1 + os.path.basename(base) + t.getName()
962	ArchTmpDir2 = tmpDir2 + os.path.basename(base) + t.getName()
963
964	#
965	# Be optimistic and first try a straight file compare
966	# as it will allow us to finish up quickly.
967	#
968	if compareBasic(base, ptch, True, fileType) == 0 :
969		return 0
970
971	try:
972		os.makedirs(ArchTmpDir1)
973	except OSError as e:
974		error("compareArchives: makedir failed %s" % e)
975		return -1
976	try:
977		os.makedirs(ArchTmpDir2)
978	except OSError as e:
979		error("compareArchives: makedir failed %s" % e)
980		return -1
981
982	# copy over the objects to the temp areas, and
983	# unpack them
984	baseCmd = "cp -fp " + base + " " + ArchTmpDir1
985	rc, output = getoutput(baseCmd)
986	if rc != 0:
987		error(baseCmd + " failed: " + output)
988		clearTmpDirs(ArchTmpDir1, ArchTmpDir2)
989		return -1
990
991	ptchCmd = "cp -fp " + ptch + " " + ArchTmpDir2
992	rc, output = getoutput(ptchCmd)
993	if rc != 0:
994		error(ptchCmd + " failed: " + output)
995		clearTmpDirs(ArchTmpDir1, ArchTmpDir2)
996		return -1
997
998	bname = fileName.split('/')[-1]
999	if fileType == "Java Archive" :
1000		baseCmd = ("cd " + ArchTmpDir1 + "; " + "jar xf " + bname +
1001			  "; rm -f " + bname + " META-INF/MANIFEST.MF")
1002		ptchCmd = ("cd " + ArchTmpDir2 + "; " + "jar xf " + bname +
1003			  "; rm -f " + bname + " META-INF/MANIFEST.MF")
1004	elif fileType == "ELF Object Archive" :
1005		baseCmd = ("cd " + ArchTmpDir1 + "; " + "/usr/ccs/bin/ar x " +
1006			  bname + "; rm -f " + bname)
1007		ptchCmd = ("cd " + ArchTmpDir2 + "; " + "/usr/ccs/bin/ar x " +
1008			  bname + "; rm -f " + bname)
1009	else :
1010		error("unexpected file type: " + fileType)
1011		clearTmpDirs(ArchTmpDir1, ArchTmpDir2)
1012		return -1
1013
1014	os.system(baseCmd)
1015	os.system(ptchCmd)
1016
1017	baseFlist = list(findFiles(ArchTmpDir1))
1018	ptchFlist = list(findFiles(ArchTmpDir2))
1019
1020	# Trim leading path off base/ptch file lists
1021	flist = []
1022	for fn in baseFlist :
1023		flist.append(str_prefix_trunc(fn, ArchTmpDir1))
1024	baseFlist = flist
1025
1026	flist = []
1027	for fn in ptchFlist :
1028		flist.append(str_prefix_trunc(fn, ArchTmpDir2))
1029	ptchFlist = flist
1030
1031	for fn in ptchFlist :
1032		if not fn in baseFlist :
1033			difference(fileName, fileType,
1034				   fn + " added to " + fileName)
1035			clearTmpDirs(ArchTmpDir1, ArchTmpDir2)
1036			return 1
1037
1038	for fn in baseFlist :
1039		if not fn in ptchFlist :
1040			difference(fileName, fileType,
1041				   fn + " removed from " + fileName)
1042			clearTmpDirs(ArchTmpDir1, ArchTmpDir2)
1043			return 1
1044
1045		differs = compareOneFile((ArchTmpDir1 + fn),
1046		    (ArchTmpDir2 + fn), True)
1047		if differs :
1048			difference(fileName, fileType,
1049				   fn + " in " + fileName + " differs")
1050			clearTmpDirs(ArchTmpDir1, ArchTmpDir2)
1051			return 1
1052
1053	clearTmpDirs(ArchTmpDir1, ArchTmpDir2)
1054	return 0
1055
1056#####
1057# (Basic) file comparison
1058#
1059# There's some special case code here for Javadoc HTML files
1060#
1061# Returns 1 if difference detected
1062#         0 if no difference detected
1063#        -1 on error
1064#
1065def compareBasic(base, ptch, quiet, fileType) :
1066
1067	fileName = fnFormat(base);
1068
1069	if quiet and os.stat(base)[ST_SIZE] != os.stat(ptch)[ST_SIZE] :
1070		return 1
1071
1072	try:
1073		baseFile = io.open(base, errors='replace')
1074	except:
1075		error("could not open " + base)
1076		return -1
1077	try:
1078		ptchFile = io.open(ptch, errors='replace')
1079	except:
1080		error("could not open " + ptch)
1081		return -1
1082
1083	baseData = baseFile.read()
1084	ptchData = ptchFile.read()
1085
1086	baseFile.close()
1087	ptchFile.close()
1088
1089	needToSnip = False
1090	if fileType == "HTML" :
1091		needToSnip = True
1092		toSnipBeginStr = "<!-- Generated by javadoc"
1093		toSnipEndStr = "-->\n"
1094
1095	if needToSnip :
1096		toSnipBegin = baseData.find(toSnipBeginStr)
1097		if toSnipBegin != -1 :
1098			toSnipEnd = (baseData[toSnipBegin:].find(toSnipEndStr) +
1099						len(toSnipEndStr))
1100			baseData = (baseData[:toSnipBegin] +
1101				   baseData[toSnipBegin + toSnipEnd:])
1102			ptchData = (ptchData[:toSnipBegin] +
1103				   ptchData[toSnipBegin + toSnipEnd:])
1104
1105	if quiet :
1106		if baseData != ptchData :
1107			return 1
1108	else :
1109		if len(baseData) != len(ptchData) or baseData != ptchData :
1110			diffs = diffData(base, ptch, baseData, ptchData)
1111			difference(fileName, fileType, diffs)
1112			return 1
1113	return 0
1114
1115
1116#####
1117# Compare two objects by producing a data dump from
1118# each object, and then comparing the dump data
1119#
1120# Returns: 1 if a difference is detected
1121#          0 if no difference detected
1122#         -1 upon error
1123#
1124def compareByDumping(base, ptch, quiet, fileType) :
1125
1126	fileName = fnFormat(base);
1127	t = threading.currentThread()
1128	tmpFile1 = tmpDir1 + os.path.basename(base) + t.getName()
1129	tmpFile2 = tmpDir2 + os.path.basename(ptch) + t.getName()
1130
1131	if fileType == "Lint Library" :
1132		baseCmd = (lintdump_cmd + " -ir " + base +
1133			  " | egrep -v '(LINTOBJ|LINTMOD):'" +
1134			  " | grep -v PASS[1-3]:" +
1135			  " > " + tmpFile1)
1136		ptchCmd = (lintdump_cmd + " -ir " + ptch +
1137			  " | egrep -v '(LINTOBJ|LINTMOD):'" +
1138			  " | grep -v PASS[1-3]:" +
1139			  " > " + tmpFile2)
1140	elif fileType == "Sqlite Database" :
1141		baseCmd = ("echo .dump | " + sqlite_cmd + base + " > " +
1142			  tmpFile1)
1143		ptchCmd = ("echo .dump | " + sqlite_cmd + ptch + " > " +
1144			  tmpFile2)
1145
1146	os.system(baseCmd)
1147	os.system(ptchCmd)
1148
1149	try:
1150		baseFile = open(tmpFile1)
1151	except:
1152		error("could not open: " + tmpFile1)
1153		return
1154	try:
1155		ptchFile = open(tmpFile2)
1156	except:
1157		error("could not open: " + tmpFile2)
1158		return
1159
1160	baseData = baseFile.read()
1161	ptchData = ptchFile.read()
1162
1163	baseFile.close()
1164	ptchFile.close()
1165
1166	if len(baseData) != len(ptchData) or baseData != ptchData :
1167		if not quiet :
1168			data = diffFileData(tmpFile1, tmpFile2);
1169			try:
1170				os.unlink(tmpFile1)
1171			except OSError as e:
1172				error("compareByDumping: unlink failed %s" % e)
1173			try:
1174				os.unlink(tmpFile2)
1175			except OSError as e:
1176				error("compareByDumping: unlink failed %s" % e)
1177			difference(fileName, fileType, data)
1178		return 1
1179
1180	# Remove the temporary files now.
1181	try:
1182		os.unlink(tmpFile1)
1183	except OSError as e:
1184		error("compareByDumping: unlink failed %s" % e)
1185	try:
1186		os.unlink(tmpFile2)
1187	except OSError as e:
1188		error("compareByDumping: unlink failed %s" % e)
1189
1190	return 0
1191
1192#####
1193#
1194# SIGINT signal handler. Changes thread control variable to tell the threads
1195# to finish their current job and exit.
1196#
1197def discontinue_processing(signl, frme):
1198	global keep_processing
1199
1200	print("Caught Ctrl-C, stopping the threads", file=sys.stderr)
1201	keep_processing = False
1202
1203	return 0
1204
1205#####
1206#
1207# worker thread for changedFiles processing
1208#
1209class workerThread(threading.Thread) :
1210	def run(self):
1211		global wset_lock
1212		global changedFiles
1213		global baseRoot
1214		global ptchRoot
1215		global keep_processing
1216
1217		while (keep_processing) :
1218			# grab the lock to changedFiles and remove one member
1219			# and process it
1220			wset_lock.acquire()
1221			try :
1222				fn = changedFiles.pop()
1223			except IndexError :
1224				# there is nothing more to do
1225				wset_lock.release()
1226				return
1227			wset_lock.release()
1228
1229			base = baseRoot + fn
1230			ptch = ptchRoot + fn
1231
1232			compareOneFile(base, ptch, False)
1233
1234
1235#####
1236# Compare two objects. Detect type changes.
1237# Vector off to the appropriate type specific
1238# compare routine based on the type.
1239#
1240def compareOneFile(base, ptch, quiet) :
1241
1242	# Verify the file types.
1243	# If they are different, indicate this and move on
1244	btype = getTheFileType(base)
1245	ptype = getTheFileType(ptch)
1246
1247	if btype == 'Error' or ptype == 'Error' :
1248		return -1
1249
1250	fileName = fnFormat(base)
1251
1252	if (btype != ptype) :
1253		if not quiet :
1254			difference(fileName, "file type", btype + " to " + ptype)
1255		return 1
1256	else :
1257		fileType = btype
1258
1259	if (fileType == 'ELF') :
1260		return compareElfs(base, ptch, quiet)
1261
1262	elif (fileType == 'Java Archive' or fileType == 'ELF Object Archive') :
1263		return compareArchives(base, ptch, fileType)
1264
1265	elif (fileType == 'HTML') :
1266		return compareBasic(base, ptch, quiet, fileType)
1267
1268	elif ( fileType == 'Lint Library' ) :
1269		return compareByDumping(base, ptch, quiet, fileType)
1270
1271	elif ( fileType == 'Sqlite Database' ) :
1272		return compareByDumping(base, ptch, quiet, fileType)
1273
1274	else :
1275		# it has to be some variety of text file
1276		return compareBasic(base, ptch, quiet, fileType)
1277
1278# Cleanup and self-terminate
1279def cleanup(ret) :
1280
1281	debug("Performing cleanup (" + str(ret) + ")")
1282	if os.path.isdir(tmpDir1) > 0 :
1283		shutil.rmtree(tmpDir1)
1284
1285	if os.path.isdir(tmpDir2) > 0 :
1286		shutil.rmtree(tmpDir2)
1287
1288	if logging :
1289		log.close()
1290
1291	sys.exit(ret)
1292
1293def main() :
1294
1295	# Log file handle
1296	global log
1297
1298	# Globals relating to command line options
1299	global logging, vdiffs, reportAllSects
1300
1301	# Named temporary files / directories
1302	global tmpDir1, tmpDir2
1303
1304	# Command paths
1305	global lintdump_cmd, elfdump_cmd, dump_cmd, dis_cmd, od_cmd, diff_cmd, sqlite_cmd
1306
1307	# Default search path
1308	global wsdiff_path
1309
1310	# Essentially "uname -p"
1311	global arch
1312
1313	# changed files for worker thread processing
1314	global changedFiles
1315	global baseRoot
1316	global ptchRoot
1317
1318	# Sort the list of files from a temporary file
1319	global sorted
1320	global differentFiles
1321
1322	# Debugging indicator
1323	global debugon
1324
1325	# Some globals need to be initialized
1326	debugon = logging = vdiffs = reportAllSects = sorted = False
1327
1328
1329	# Process command line arguments
1330	# Return values are returned from args() in alpha order
1331	# (Yes, python functions can return multiple values (ewww))
1332	# Note that args() also set the globals:
1333	#	logging to True if verbose logging (to a file) was enabled
1334	#	vdiffs to True if logged differences aren't to be truncated
1335	#	reportAllSects to True if all ELF section differences are to be reported
1336	#
1337	baseRoot, fileNamesFile, localTools, ptchRoot, results = args()
1338
1339	#
1340	# Set up the results/log file
1341	#
1342	if logging :
1343		try:
1344			log = open(results, "w")
1345		except:
1346			logging = False
1347			error("failed to open log file: " + log)
1348			sys.exit(1)
1349
1350		dateTimeStr= "# %04d-%02d-%02d at %02d:%02d:%02d" % time.localtime()[:6]
1351		v_info("# This file was produced by wsdiff")
1352		v_info(dateTimeStr)
1353
1354	# Changed files (used only for the sorted case)
1355	if sorted :
1356		differentFiles = []
1357
1358	#
1359	# Build paths to the tools required tools
1360	#
1361	# Try to look for tools in $SRC/tools if the "-t" option
1362	# was specified
1363	#
1364	rc, arch = getoutput("uname -p")
1365	arch = arch.rstrip()
1366	if localTools :
1367		try:
1368			src = os.environ['SRC']
1369		except:
1370			error("-t specified, but $SRC not set. Cannot find $SRC/tools")
1371			src = ""
1372		if len(src) > 0 :
1373			wsdiff_path.insert(0, src + "/tools/proto/opt/onbld/bin")
1374
1375	lintdump_cmd = find_tool("lintdump")
1376	elfdump_cmd = find_tool("elfdump")
1377	dump_cmd = find_tool("dump")
1378	od_cmd = find_tool("od")
1379	dis_cmd = find_tool("dis")
1380	diff_cmd = find_tool("diff")
1381	sqlite_cmd = find_tool("sqlite")
1382
1383	#
1384	# Set resource limit for number of open files as high as possible.
1385	# This might get handy with big number of threads.
1386	#
1387	(nofile_soft, nofile_hard) = resource.getrlimit(resource.RLIMIT_NOFILE)
1388	try:
1389		resource.setrlimit(resource.RLIMIT_NOFILE,
1390		    (nofile_hard, nofile_hard))
1391	except:
1392		error("cannot set resource limits for number of open files")
1393		sys.exit(1)
1394
1395	#
1396	# validate the base and patch paths
1397	#
1398	if baseRoot[-1] != '/' :
1399		baseRoot += '/'
1400
1401	if ptchRoot[-1] != '/' :
1402		ptchRoot += '/'
1403
1404	if not os.path.exists(baseRoot) :
1405		error("old proto area: " + baseRoot + " does not exist")
1406		sys.exit(1)
1407
1408	if not os.path.exists(ptchRoot) :
1409		error("new proto area: " + ptchRoot + " does not exist")
1410		sys.exit(1)
1411
1412	#
1413	# log some information identifying the run
1414	#
1415	v_info("Old proto area: " + baseRoot)
1416	v_info("New proto area: " + ptchRoot)
1417	v_info("Results file: " + results + "\n")
1418
1419	#
1420	# Set up the temporary directories / files
1421	# Could use python's tmpdir routines, but these should
1422	# be easier to identify / keep around for debugging
1423	pid = os.getpid()
1424	tmpDir1 = "/tmp/wsdiff_tmp1_" + str(pid) + "/"
1425	tmpDir2 = "/tmp/wsdiff_tmp2_" + str(pid) + "/"
1426	try:
1427		os.makedirs(tmpDir1)
1428	except OSError as e:
1429		error("main: makedir failed %s" % e)
1430	try:
1431		os.makedirs(tmpDir2)
1432	except OSError as e:
1433		error("main: makedir failed %s" % e)
1434
1435	# Derive a catalog of new, deleted, and to-be-compared objects
1436	# either from the specified base and patch proto areas, or from
1437	# from an input file list
1438	newOrDeleted = False
1439
1440	if fileNamesFile != "" :
1441		changedFiles, newFiles, deletedFiles = \
1442			      flistCatalog(baseRoot, ptchRoot, fileNamesFile)
1443	else :
1444		changedFiles, newFiles, deletedFiles = \
1445				protoCatalog(baseRoot, ptchRoot)
1446
1447	if len(newFiles) > 0 :
1448		newOrDeleted = True
1449		info("\nNew objects found: ")
1450
1451		if sorted :
1452			newFiles.sort()
1453		for fn in newFiles :
1454			info(fnFormat(fn))
1455
1456	if len(deletedFiles) > 0 :
1457		newOrDeleted = True
1458		info("\nObjects removed: ")
1459
1460		if sorted :
1461			deletedFiles.sort()
1462		for fn in deletedFiles :
1463			info(fnFormat(fn))
1464
1465	if newOrDeleted :
1466		info("\nChanged objects: ")
1467	if sorted :
1468		debug("The list will appear after the processing is done")
1469
1470	# Here's where all the heavy lifting happens
1471	# Perform a comparison on each object appearing in
1472	# both proto areas. compareOneFile will examine the
1473	# file types of each object, and will vector off to
1474	# the appropriate comparison routine, where the compare
1475	# will happen, and any differences will be reported / logged
1476
1477	# determine maximum number of worker threads by using
1478	# DMAKE_MAX_JOBS environment variable set by nightly(1)
1479	# or get number of CPUs in the system
1480	try:
1481		max_threads = int(os.environ['DMAKE_MAX_JOBS'])
1482	except:
1483		max_threads = os.sysconf("SC_NPROCESSORS_ONLN")
1484		# If we cannot get number of online CPUs in the system
1485		# run unparallelized otherwise bump the number up 20%
1486		# to achieve best results.
1487		if max_threads == -1 :
1488			max_threads = 1
1489		else :
1490			max_threads += max_threads/5
1491
1492	# Set signal handler to attempt graceful exit
1493	debug("Setting signal handler")
1494	signal.signal( signal.SIGINT, discontinue_processing )
1495
1496	# Create and unleash the threads
1497	# Only at most max_threads must be running at any moment
1498	mythreads = []
1499	debug("Spawning " + str(max_threads) + " threads");
1500	for i in range(max_threads) :
1501		thread = workerThread()
1502		mythreads.append(thread)
1503		mythreads[i].start()
1504
1505	# Wait for the threads to finish and do cleanup if interrupted
1506	debug("Waiting for the threads to finish")
1507	while True:
1508		if not True in [thread.isAlive() for thread in mythreads]:
1509		    break
1510		else:
1511		    # Some threads are still going
1512		    time.sleep(1)
1513
1514	# Interrupted by SIGINT
1515	if keep_processing == False :
1516		cleanup(1)
1517
1518	# If the list of differences was sorted it is stored in an array
1519	if sorted :
1520		differentFiles.sort()
1521		for f in differentFiles :
1522			info(fnFormat(f))
1523
1524	# We're done, cleanup.
1525	cleanup(0)
1526
1527if __name__ == '__main__' :
1528	try:
1529		main()
1530	except KeyboardInterrupt :
1531		cleanup(1);
1532
1533