Skip site navigation (1)Skip section navigation (2)
Date:      Fri, 12 Jan 2001 19:15:04 +0200
From:      Maxim Sobolev <sobomax@FreeBSD.org>
To:        ports@FreeBSD.org
Subject:   RFC: tool to automate common operations with patchfiles
Message-ID:  <3A5F3B98.954D80A5@FreeBSD.org>

next in thread | raw e-mail | index | archive | help
This is a multi-part message in MIME format.
--------------0CE4933781104F1CFCC44A6F
Content-Type: text/plain; charset=koi8-r
Content-Transfer-Encoding: 7bit

Hi folks,

With this message I'm attaching a new tool aimed to significantly simplify
common operations with ports' patches. I modelled it based on my own experience
with the subject, so I hope it would be useful for all hardcore porters and
would like to hear your comments/suggestions/patches/flames etc.

The tool has the following two modes of operation:

1. Generate a patchfile(a) from the user supplied file(s).
----------------------------------------------------------
Following example session should give you an idea on how it operates:
$ pwd
/somewhere/myport/work/myport-1.0/src/lib/bar
$ cp foo.c foo.c.orig
$ vi foo.c
[..] <-make your changes
$ patchtool foo.c | more
[..] <-read the diff
$ patchtool -a foo.c
Generating patchfile: patch-src_lib_bar_foo.c <- now you have patchfile ready
in /somewhere/myport/files
[..]

The tool tries hard to guess correct path of the file relatively to WRKSRC, so
you current directory does not matter and patch should be generated properly
wherever you are, so example, the following will work all the same:

$ cd /to/the/hell
$ patchtool -a /somewhere/myport/work/myport-1.0/src/lib/bar/foo.c
Generating patchfile: patch-src_lib_bar_foo.c...
$ patchtool -a ../../../somewhere/myport/work/myport-1.0/src/lib/bar/foo.c
Generating patchfile: patch-src_lib_bar_foo.c...

Several notes:
- `-a' option instructs the tool to automatically save resulting diff into
appropriate patchfile instead of dumping it to the stdout;

- the tool is smart enough to check your existing patches before generating
patchfile, so for example if in the previous case you already had a patch for
src/lib/bar/foo.c called patch-xxx, then it would update that existing
patchfile instead of creating a new one;

- the tool honours WRKDIRPREFIX;

- you can supply arbitrary number of files to generate patches from, you can
even mix files from the different ports - the resulting patchfiles will be
placed into the appropriate PATCHDIRs properly, for example:

$ patchtool -a port1/work/port1-1.0/1.c port2/work/port2-1.0/2.c
Generating patchfile: patch-1.c...
Generating patchfile: patch-2.c...

will generate port1/files/patch-1.c and port2/files/patch-2.c.


2. Update pointed by the user patchfile(s).
-------------------------------------------
Another mode of operation is updating of already existing patchfiles. In this
mode the tool updates all patchfiles specified in the command line, for
example:

$ patchtool -u files/patch-aa files/patch-be
Updating patchfile: patch-aa...
Updating patchfile: patch-be...

Alternatively you can specify any directory within a tree of a particular port,
in that case all patches that belong to this port will be updated, for example:

$ ls /somewhere/myport/files
patch-1 patch-8
$ patchtool -u /somewhere/myport
Updating patchfile: patch-1...
Updating patchfile: patch-8...
$ patchtool -u /somewhere/myport/files
Updating patchfile: patch-1...
Updating patchfile: patch-8...
$ cd /somewhere/myport/work
$ patchtool -u ./
Updating patchfile: patch-1...
Updating patchfile: patch-8...
[etc]

Final notes:
- you can tweak many aspects of behaviour, e.g. default diff flags (/me hands
will Will PT_DIFF_ARGS=-u0), patchfile prefix, suffix for saved version of
files etc., by setting appropriate environment variable. See comments at the
very beginning of the file;

- error handling may not be ideal at the moment - please report any problems
you may have with it;

- the tool currently doesn't try to make sure that the patch is in fact needs
to be updated, so use -u for existing ports with prejudice (it may generate
unnecessary repobloat);

- sourcecode is best viewed with tabsize=4.


Regards,

Maxim
P.S. Any flames/holywars regarding Python vs. Perl/Ruby/whatever will be
silently dumped to /dev/null! :-)
--------------0CE4933781104F1CFCC44A6F
Content-Type: text/plain; charset=koi8-r;
 name="patchtool"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="patchtool"

#!/usr/local/bin/python
#
# ----------------------------------------------------------------------------
# "THE BEER-WARE LICENSE" (Revision 42, (c) Poul-Henning Kamp):
# Maxim Sobolev <sobomax@FreeBSD.org> wrote this file.  As long as you retain
# this notice you can do whatever you want with this stuff. If we meet some
# day, and you think this stuff is worth it, you can buy me a beer in return.
#
# Maxim Sobolev
# ----------------------------------------------------------------------------
#
# $FreeBSD$
#
# MAINTAINER= sobomax@FreeBSD.org
#

import os, os.path, popen2, sys, getopt, glob, errno, types

# Some global variables used as constants
True = 1
False = 0
Error = -1


# Tweakable global variables. User is able to override any of these by setting
# appropriate environment variable prefixed by `PT_', eg:
# $ export PT_CVS_ID="FooOS"
# $ export PT_DIFF_CMD="/usr/local/bin/mydiff"
# will force script to use "FooOS" as a CVS_ID instead of "FreeBSD" and
# "/usr/local/bin/mydiff" as a command to generate diffs.
class Vars:
	CVS_ID = 'FreeBSD'

	DIFF_ARGS = '-du'
	DIFF_SUFX = '.orig'
	PATCH_PREFIX = 'patch-'
	RCSDIFF_SUFX = ',v'

	CD_CMD = 'cd'
	DIFF_CMD = '/usr/bin/diff'
	MAKE_CMD = '/usr/bin/make'
	PRINTF_CMD = '/usr/bin/printf'
	RCSDIFF_CMD = '/usr/bin/rcsdiff'

	DEFAULT_MAKEFILE = 'Makefile'
	DEV_NULL = '/dev/null'
	ETC_MAKE_CONF = '/etc/make.conf'


#
# Check if the supplied patch refers to a port's directory.
#
def isportdir(path, soft = False):
	REQ_FILES = ('Makefile', 'pkg-comment', 'pkg-descr', 'pkg-plist', \
      'distinfo')
	if not os.path.isdir(path) and soft != True:
		raise IOError(errno.ENOENT, path)
		# Not reached #

	try:
		content = os.listdir(path)
	except OSError:
		return False

	for file in REQ_FILES:
		if file not in content:
			return False
	return True


#
# Traverse directory tree up from the path pointed by argument and return if
# root directory of a port is found.
#
def locateportdir(path, wrkdirprefix= ''):
	# Flag to relax error checking in isportdir() function. It required when
	# WRKDIRPREFIX is defined.
	softisport = False

	path = os.path.abspath(path)

	if wrkdirprefix != '':
		wrkdirprefix= os.path.abspath(wrkdirprefix)
		commonprefix = os.path.commonprefix((path, wrkdirprefix))
		if commonprefix != wrkdirprefix:
			return ''
		path = path[len(wrkdirprefix):]
		softisport = True
	while path != '/':
		if isportdir(path, softisport) == True:
			return path
		path = os.path.abspath(os.path.join(path, '..'))
	return ''


#
# Get value of a make(1) variable called varname. Optionally maintain a cache
# for resolved varname:makepath pairs to speed-up operation if the same variable
# from the exactly same file is requiested repeatedly (invocation of make(1) is
# very expensive operation...)
#
def querymakevar(varname, path = 'Makefile', cache = {}):
	path = os.path.abspath(path)

	if cache.has_key((varname, path)) == 1:
		return cache[(varname, path)]

	origpath = path
	if os.path.isdir(path):
		path = os.path.join(path, Vars.DEFAULT_MAKEFILE)
	if not os.path.isfile(path):
		raise IOError(errno.ENOENT, path)
		# Not reached #
	dir = os.path.dirname(path)
	CMDLINE = '%s %s && %s -f %s -V %s' % (Vars.CD_CMD, dir, Vars.MAKE_CMD, \
      path, varname)
	pipe = popen2.popen3(CMDLINE)
	retval = ''
	for line in pipe[0].readlines():
		retval = retval + line.strip() + ' '
	for fd in pipe:
		fd.close()
	retval = retval[:-1]
	cache[(varname, origpath)] = retval
	return retval


#
# Get a path of `path'  relatively to wrksrc. For example:
# path:		/foo/bar
# wrksrc:	/foo/bar/baz/somefile.c
# getrelpath:	baz/somefile.c
# Most of the code here is to handle cases when ../ operation is required to
# reach wrksrc from path, for example:
# path:		/foo/bar
# wrksrc:	/foo/baz/somefile.c
# getrelpath:	../baz/somefile.c
#
def getrelpath(path, wrksrc):
	path = os.path.abspath(path)
	wrksrc = os.path.abspath(wrksrc)
	commonpart = os.path.commonprefix((path, wrksrc))
	path = path[len(commonpart):]
	wrksrc = wrksrc[len(commonpart):]
	if wrksrc == '':
		path = path[1:]
	adjust = ''
	while os.path.normpath(os.path.join(wrksrc, adjust)) != '.':
		adjust = os.path.join(adjust, '..')
	relpath = os.path.join(adjust, path)
	return relpath


#
# Generare a diff between saved and current versions of the file pointed by the
# wrksrc+path. Apply heuristics to locate saved version of the file in question
# and if it fails assume that file is new, so /dev/null is to be used as
# original file. Optionally save generated patch into `outfile' instead of
# dumping it to stdout. Generated patches automatically being tagged with
# "FreeBSD" cvs id.
#
def gendiff(path, wrksrc, outfile = ''):
	IDGEN_CMD = '%s "\n\$%s\$\n\n"' % (Vars.PRINTF_CMD, Vars.CVS_ID)

	fullpath = os.path.join(wrksrc, path)
	if not os.path.isfile(fullpath):
		raise IOError(errno.ENOENT, fullpath)
		# Not reached #

	retval = Error
	cmdline = ''
	if os.path.isfile(fullpath + Vars.DIFF_SUFX):		# Normal diff
		cmdline = '%s %s %s%s %s' % (Vars.DIFF_CMD, Vars.DIFF_ARGS, path, \
		  Vars.DIFF_SUFX, path)
	elif os.path.isfile(fullpath + Vars.RCSDIFF_SUFX):	# RCS diff
		cmdline = '%s %s %s' % (Vars.RCSDIFF_CMD, Vars.DIFF_ARGS, path)
	else:												# New file
		cmdline = '%s %s %s %s' % (Vars.DIFF_CMD, Vars.DIFF_ARGS, \
          Vars.DEV_NULL, path)

	if cmdline != '':
		if outfile != '':
			cmdline = '( %s && %s ) 1>%s 2>%s' % (IDGEN_CMD, cmdline, outfile, \
			  Vars.DEV_NULL)
		savedir = os.getcwd()
		os.chdir(wrksrc)
		exitstat = os.system(cmdline)
		if os.WIFEXITED(exitstat):
			exitval = os.WEXITSTATUS(exitstat)
			if exitval == 0:    # No differences were found
				if outfile != '':
					os.unlink(outfile)
				retval = False
			elif exitval == 1:  # Some differences  were  found
				retval = True
			else:               # Error occured
				raise ECmdError('"%s"' % cmdline, \
				  'external command returned non-zero error code')
		os.chdir(savedir)
	return retval


#
# Automatically generate a name for a patch based on its path relative to
# wrksrc. Use simple scheme to ensute 1-to-1 mapping between path and
# patchname - replace all '_' with '__' and all '/' with '_'.
# 
def makepatchname(path, patchdir = ''):
	retval = Vars.PATCH_PREFIX + \
	  path.replace('_', '__').replace('/', '_')
	retval = os.path.join(patchdir, retval)
	return retval


#
# Write a specified message to stderr.
#
def write_msg(message):
	if type(message) == types.StringType:
		message = message,
	sys.stderr.writelines(message)


#
# Print specified message to stdout and ask user [y/N]?. Optionally allow
# specify default answer, i.e. return value if user typed only <cr>
#
def query_yn(message, default = False):
	while True:
		if default == True:
			yn = 'Y/n'
		elif default == False:
			yn = 'y/N'
		else:
			yn = 'Y/N'

		reply = raw_input('%s [%s]: ' % (message, yn))

		if reply == 'y' or reply == 'Y':
			return True
		elif reply == 'n' or reply == 'N':
			return False
		elif reply == '' and default in (True, False):
			return default
		print 'Wrong answer "%s", please try again' % reply


#
# Print optional message and usage information and exit with specified exit
# code.
#
def usage(code, msg = ''):
	myname = os.path.basename(sys.argv[0])
	write_msg((str(msg), """
Usage: %s [-ai] file ...
       %s -u [ai] [patchfile|patchdir ...]
""" % (myname, myname)))
	sys.exit(code)


#
# Simple custom exception
#
class MyError:
	msg = 'error'

	def __init__(self, file, msg=''):
		self.file = file
		if msg != '':
			self.msg = msg

	def __str__(self):
			return '%s: %s' % (self.file, self.msg)


#
# Error parsing patchfile
#
class PatchError(MyError):
	msg = 'corrupt patchfile'


#
# Error executing external command
#
class ECmdError(MyError):
	pass


class Patch:
	fullpath = ''
	minus3file = ''
	plus3file = ''
	wrksrc = ''

	def __init__(self, path, wrksrc):
		MINUS3_DELIM = '--- '
		PLUS3_DELIM = '+++ '

		path = os.path.abspath(path)
		if not os.path.isfile(path):
			raise IOError(errno.ENOENT, path)
			# Not reached #
		self.fullpath = path
		file = open(path)

		for line in file.readlines():
			if self.minus3file == '':
				if line[:len(MINUS3_DELIM)] == MINUS3_DELIM:
					lineparts = line.split()
					try:
						self.minus3file = lineparts[1]
					except IndexError:
						raise PatchError(path)
						# Not reached #
					continue
			elif line[:len(PLUS3_DELIM)] == PLUS3_DELIM:
				lineparts = line.split()
				try:
					self.plus3file = lineparts[1]
				except IndexError:
					raise PatchError(path)
					# Not reached #
				break

		file.close()
		if self.minus3file == '' or self.plus3file == '':
			raise PatchError(path)
			# Not reached #
		self.wrksrc = os.path.abspath(wrksrc)

	def update(self):
		targetfile = os.path.join(self.wrksrc, self.plus3file)
		if not os.path.isfile(targetfile):
			raise IOError(errno.ENOENT, targetfile)
		patchdir = os.path.dirname(self.fullpath)
		if not os.path.isdir(patchdir):
			os.mkdir(patchdir)
		return gendiff(self.plus3file, self.wrksrc, self.fullpath)


class NewPatch(Patch):
	def __init__(self, patchdir, wrksrc, relpath):
		self.fullpath = makepatchname(relpath, os.path.abspath(patchdir))
		self.wrksrc = os.path.abspath(wrksrc)
		self.plus3file = relpath
		self.minus3file = relpath


class PatchesCollection:
	patches = {}

	def __init__(self):
		self.patches = {}
		pass

	def adddir(self, patchdir, wrksrc):
		if not os.path.isdir(patchdir):
			raise IOError(errno.ENOENT, patchdir)
			# Not reached #
		for file in glob.glob(os.path.join(patchdir, Vars.PATCH_PREFIX + '*')):
			self.addpatchfile(file, wrksrc)

	def addpatchfile(self, path, wrksrc):
		path = os.path.abspath(path)
		if not self.patches.has_key(path):
			self.addpatchobj(Patch(path, wrksrc))

	def addpatchobj(self, patchobj):
		self.patches[patchobj.fullpath] = patchobj

	def lookupbyname(self, path):
		path = os.path.abspath(path)
		if self.patches.has_key(path):
			return self.patches[path]
		return None

	def lookupbytarget(self, wrksrc, relpath):
		wrksrc = os.path.abspath(wrksrc)
		for patch in self.patches.values():
			if wrksrc == patch.wrksrc and relpath == patch.plus3file:
				return patch
		return None

	def getpatchobjs(self):
		return self.patches.values()


def main():
	try:
		opts, args = getopt.getopt(sys.argv[1:], 'afu')
	except getopt.GetoptError, msg:
		usage(2, msg)

	automatic = False
	force = False
	mode = generate

	for o, a in opts:
		if o == '-a':
			automatic = True
		elif o == '-f':
			force = True
		elif o == '-u':
			mode = update
		else:
			usage(2)

	# Allow user to override internal constants
	for varname in dir(Vars):
		if varname[:2] == '__' and varname[-2:] == '__':
			continue
		try:
			value = os.environ['PT_' + varname]
			setattr(Vars, varname, value)
		except KeyError:
			pass

	mode(args, automatic, force)

	sys.exit(0)


def generate(args, automatic, force):
	if len(args) == 0:
		usage(2, "ERROR: no input files specified")

	patches = PatchesCollection()

	for filepath in args:
		if not os.path.isfile(filepath):
			raise IOError(errno.ENOENT, filepath)
			# Not reached #

		wrkdirprefix = querymakevar('WRKDIRPREFIX', Vars.ETC_MAKE_CONF)

		portdir = locateportdir(os.path.dirname(filepath), wrkdirprefix)
		if portdir == '':
			sys.exit("ERROR: can't locate portdir")
			# Not reached #

		wrksrc = querymakevar('WRKSRC', portdir)
		if wrksrc == '':
			sys.exit("ERROR: can't get WRKSRC value")
			# Not reached #

		relpath = getrelpath(filepath, wrksrc)

		if automatic == True:
			patchdir = querymakevar('PATCHDIR', portdir)
			if patchdir == '':
				sys.exit("ERROR: can't get PATCHDIR value")
				# Not reached #

			if os.path.isdir(patchdir):
				patches.adddir(patchdir, wrksrc)

			patchobj = patches.lookupbytarget(wrksrc, relpath)
			if patchobj == None:
				patchobj = NewPatch(patchdir, wrksrc, relpath)
				patches.addpatchobj(patchobj)

			write_msg('Generating patchfile: %s...\n' % \
			  os.path.basename(patchobj.fullpath))

			if not force and os.path.exists(patchobj.fullpath) and \
			  os.path.getsize(patchobj.fullpath) > 0:
				try:
					retval = query_yn('Target patchfile "%s" already ' \
					  'exists, do you want to  replace it?' % \
					  os.path.basename(patchobj.fullpath))
				except KeyboardInterrupt:
					sys.exit('\nAction aborted')
					# Not reached #
				if retval == False:
					continue

			retval = patchobj.update()

		else:	# automatic != True
			retval = gendiff(relpath, wrksrc)

		if retval == Error:
			sys.exit("ERROR: can't generate diff for \"%s\"" % filepath)
		elif retval == False:
			write_mss('WARNING: no differencies found between original and '
              'current version of "%s"\n' % filepath)


def update(args, automatic, force):
	if len(args) == 0:
		args = './',

	for path in args:
		if not os.path.exists(path):
			raise IOError(errno.ENOENT, path)
			# Not reached #

		patches = PatchesCollection()

		if os.path.isdir(path):
			for wrkdirprefix in (querymakevar('WRKDIRPREFIX', \
              Vars.ETC_MAKE_CONF), ''):
				portdir = locateportdir(path, wrkdirprefix)
				if portdir != '':
					break
			if portdir == '':
				sys.exit("ERROR: can't locate portdir")
				# Not reached #

			wrksrc = querymakevar('WRKSRC', portdir)
			if wrksrc == '':
				sys.exit("ERROR: can't get WRKSRC value")
				# Not reached #

			patchdir = querymakevar('PATCHDIR', portdir)
			if patchdir == '':
				sys.exit("ERROR: can't get PATCHDIR value")
				# Not reached #

			if os.path.isdir(patchdir):
				patches.adddir(patchdir, wrksrc)
			else:
				continue

		elif os.path.isfile(path):
			patches.addpatchfile(path)

		for patchobj in patches.getpatchobjs():
			write_msg('Updating patchfile: %s...\n' % \
			  patchobj.fullpath)
			patchobj.update()


if __name__ == '__main__':
	try:
		main()
	except (PatchError, ECmdError), msg:
		sys.exit('ERROR: ' + str(msg))
	except IOError, (code, msg):
		sys.exit('ERROR: %s: %s' % (str(msg), os.strerror(code)))

--------------0CE4933781104F1CFCC44A6F--



To Unsubscribe: send mail to majordomo@FreeBSD.org
with "unsubscribe freebsd-ports" in the body of the message




Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?3A5F3B98.954D80A5>