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