import os, os.path, subprocess, sys, getopt, glob, errno, types
compat_raw_input = input
if sys.version_info < (3,):
compat_raw_input = raw_input
def isStr(obj):
try:
return isinstance(obj, basestring)
except NameError:
return isinstance(obj, str)
class Vars:
CVS_ID = 'FreeBSD'
DIFF_ARGS = '-du'
DIFF_SUFX = '.orig'
PATCH_PREFIX = 'patch-'
PATCH_IGN_SUFX = ('.orig', '.rej')
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'
SLASH_REPL_SYMBOL = '_'
def isportdir(path, soft = False):
REQ_FILES = ('Makefile', 'pkg-descr', 'distinfo')
if not os.path.isdir(path) and soft != True:
raise IOError(errno.ENOENT, path)
try:
content = os.listdir(path)
except OSError:
return False
for file in REQ_FILES:
if file not in content:
return False
return True
def locateportdir(path, wrkdirprefix= '', strict = False):
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, '..'))
if strict == True:
raise LocatePDirError(path)
else:
return ''
def querymakevar(varname, path = 'Makefile', strict = False, cache = {}):
path = os.path.abspath(path)
if (varname, path) in cache:
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)
dir = os.path.dirname(path)
CMDLINE = '%s %s && %s -f %s -V %s' % (Vars.CD_CMD, dir, Vars.MAKE_CMD, \
path, varname)
devnull = open('/dev/null', 'a')
pipe = subprocess.Popen(CMDLINE, shell = True, stdin = subprocess.PIPE, \
stdout = subprocess.PIPE, stderr = devnull, close_fds = True)
retval = ''
for line in pipe.stdout.readlines():
retval = retval + line.decode().strip() + ' '
retval = retval[:-1]
if strict == True and retval.strip() == '':
raise MakeVarError(path, varname)
cache[(varname, origpath)] = retval
return retval
def getrelpath(path, wrksrc):
path = os.path.abspath(path)
wrksrc = os.path.abspath(wrksrc) + '/'
commonpart = os.path.commonprefix((path, wrksrc))
while commonpart[-1:] != '/':
commonpart = commonpart[:-1]
path = path[len(commonpart):]
wrksrc = wrksrc[len(commonpart):]
adjust = ''
while os.path.normpath(os.path.join(wrksrc, adjust)) != '.':
adjust = os.path.join(adjust, '..')
relpath = os.path.join(adjust, path)
return relpath
def gendiff(path, wrksrc, outfile = ''):
fullpath = os.path.join(wrksrc, path)
if not os.path.isfile(fullpath):
raise IOError(errno.ENOENT, fullpath)
cmdline = ''
if os.path.isfile(fullpath + Vars.DIFF_SUFX):
path_orig = path + Vars.DIFF_SUFX
cmdline = '%s %s %s %s' % (Vars.DIFF_CMD, Vars.DIFF_ARGS, path_orig, path)
elif os.path.isfile(fullpath + Vars.RCSDIFF_SUFX):
path_orig = path
cmdline = '%s %s %s' % (Vars.RCSDIFF_CMD, Vars.DIFF_ARGS, path)
else:
path_orig = Vars.DEV_NULL
cmdline = '%s %s %s %s' % (Vars.DIFF_CMD, Vars.DIFF_ARGS, path_orig, path)
savedir = os.getcwd()
os.chdir(wrksrc)
devnull = open('/dev/null', 'a')
pipe = subprocess.Popen(cmdline, shell = True, stdin = subprocess.PIPE, \
stdout = subprocess.PIPE, stderr = devnull, close_fds = True)
outbuf = [x.decode() for x in pipe.stdout.readlines()]
exitval = pipe.wait()
if exitval == 0:
retval = False
retmsg = 'no differences found between original and current ' \
'version of "%s"' % fullpath
elif exitval == 1:
if (outfile != ''):
outbuf[0] = '--- %s\n' % path_orig
outbuf[1] = '+++ %s\n' % path
open(outfile, 'w').writelines(outbuf)
else:
sys.stdout.writelines(outbuf)
retval = True
retmsg = ''
else:
raise ECmdError('"%s"' % cmdline, \
'external command returned non-zero error code')
os.chdir(savedir)
return (retval, retmsg)
def makepatchname(path, patchdir = ''):
SRS = Vars.SLASH_REPL_SYMBOL
retval = Vars.PATCH_PREFIX + \
path.replace(SRS, SRS + SRS).replace('/', SRS)
retval = os.path.join(patchdir, retval)
return retval
def write_msg(message):
if isStr(message):
message = message,
sys.stderr.writelines(message)
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 = compat_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)
return default
def usage(code, msg = ''):
myname = os.path.basename(sys.argv[0])
write_msg((str(msg), """
Usage: %s [-afi] file ...
%s -u [-i] [patchfile|patchdir ...]
""" % (myname, myname)))
sys.exit(code)
class MyError(Exception):
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)
class PatchError(MyError):
msg = 'corrupt patchfile, or not patchfile at all'
class ECmdError(MyError):
pass
class MakeVarError(MyError):
def __init__(self, file, makevar, msg=''):
self.file = file
if msg != '':
self.msg = msg
else:
self.msg = 'can\'t get %s value' % makevar
class LocatePDirError(MyError):
msg = 'can\'t locate portdir'
class Patch:
fullpath = ''
minus3file = ''
plus3file = ''
wrksrc = ''
patchmtime = 0
targetmtime = 0
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)
self.fullpath = path
filedes = open(path)
for line in filedes.readlines():
if self.minus3file == '':
if line[:len(MINUS3_DELIM)] == MINUS3_DELIM:
lineparts = line.split()
try:
self.minus3file = lineparts[1]
except IndexError:
raise PatchError(path)
continue
elif line[:len(PLUS3_DELIM)] == PLUS3_DELIM:
lineparts = line.split()
try:
self.plus3file = lineparts[1]
except IndexError:
raise PatchError(path)
break
filedes.close()
if self.minus3file == '' or self.plus3file == '':
raise PatchError(path)
self.wrksrc = os.path.abspath(wrksrc)
self.patchmtime = os.path.getmtime(self.fullpath)
plus3file = os.path.join(self.wrksrc, self.plus3file)
if os.path.isfile(plus3file):
self.targetmtime = os.path.getmtime(plus3file)
else:
self.targetmtime = 0
def update(self, patch_cookiemtime = 0, ignoremtime = False):
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)
if ignoremtime == True or self.patchmtime == 0 or \
self.targetmtime == 0 or \
(self.patchmtime < self.targetmtime and \
patch_cookiemtime < self.targetmtime):
retval = gendiff(self.plus3file, self.wrksrc, self.fullpath)
if retval[0] == True:
self.patchmtime = os.path.getmtime(self.fullpath)
else:
retval = (False, 'patch is already up to date')
return retval
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
self.patchmtime = 0
plus3file = os.path.join(self.wrksrc, self.plus3file)
if os.path.isfile(plus3file):
self.targetmtime = os.path.getmtime(plus3file)
else:
self.targetmtime = 0
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)
for filename in glob.glob(os.path.join(patchdir, Vars.PATCH_PREFIX + '*')):
for sufx in Vars.PATCH_IGN_SUFX:
if filename[-len(sufx):] == sufx:
write_msg('WARNING: patchfile "%s" ignored\n' % filename)
break
else:
self.addpatchfile(filename, wrksrc)
def addpatchfile(self, path, wrksrc):
path = os.path.abspath(path)
if path not in self.patches:
self.addpatchobj(Patch(path, wrksrc))
def addpatchobj(self, patchobj):
self.patches[patchobj.fullpath] = patchobj
def lookupbyname(self, path):
path = os.path.abspath(path)
if path in self.patches:
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 truepath(path):
if not os.path.isfile(path):
raise IOError(errno.ENOENT, path)
result = ''
while len(path) > 0:
path, lastcomp = os.path.split(path)
if len(lastcomp) == 0:
lastcomp = path
path = ''
result = os.path.join(lastcomp, result)
if len(path) == 0:
break
if os.path.islink(path):
linkto = os.path.normpath(os.readlink(path))
if linkto[0] != '/':
path = os.path.join(path, linkto)
else:
path = linkto
return result[:-1]
def main():
try:
opts, args = getopt.getopt(sys.argv[1:], 'afui')
except getopt.GetoptError as msg:
usage(2, msg)
automatic = False
force = False
mode = generate
ignoremtime = False
for o, a in opts:
if o == '-a':
automatic = True
elif o == '-f':
force = True
elif o == '-u':
mode = update
elif o == '-i':
ignoremtime = True
else:
usage(2)
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, ignoremtime)
sys.exit(0)
def generate(args, automatic, force, ignoremtime):
if len(args) == 0:
usage(2, "ERROR: no input files specified")
patches = PatchesCollection()
for filepath in args:
for suf in Vars.RCSDIFF_SUFX, Vars.DIFF_SUFX:
if filepath.endswith(suf):
filepath = filepath[:-len(suf)]
break
if not os.path.isfile(filepath):
raise IOError(errno.ENOENT, filepath)
filepath = truepath(filepath)
wrkdirprefix = querymakevar('WRKDIRPREFIX', Vars.ETC_MAKE_CONF, False)
portdir = locateportdir(os.path.dirname(filepath), wrkdirprefix, True)
wrksrc = querymakevar('WRKSRC', portdir, True)
relpath = getrelpath(filepath, wrksrc)
if automatic:
patchdir = querymakevar('PATCHDIR', portdir, True)
if os.path.isdir(patchdir):
patches.adddir(patchdir, wrksrc)
extra_patches = querymakevar('EXTRA_PATCHES', portdir, False)
for extra_patch in extra_patches.split():
if os.path.isfile(extra_patch):
patches.addpatchfile(extra_patch, wrksrc)
patchobj = patches.lookupbytarget(wrksrc, relpath)
if patchobj == None:
patchobj = NewPatch(patchdir, wrksrc, relpath)
patches.addpatchobj(patchobj)
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')
if retval == False:
continue
write_msg('Generating patchfile: %s...' % \
os.path.basename(patchobj.fullpath))
try:
retval = None
retval = patchobj.update(ignoremtime = ignoremtime)
finally:
if retval == None:
write_msg('OUCH!\n')
if retval[0] == False:
write_msg('skipped (%s)\n' % retval[1])
else:
write_msg('ok\n')
else:
retval = gendiff(relpath, wrksrc)
if retval[0] == False:
write_msg('WARNING: %s\n' % retval[1])
def update(args, automatic, force, ignoremtime):
if len(args) == 0:
args = './',
for path in args:
if not os.path.exists(path):
raise IOError(errno.ENOENT, path)
patches = PatchesCollection()
if os.path.isdir(path):
for wrkdirprefix in (querymakevar('WRKDIRPREFIX', \
Vars.ETC_MAKE_CONF, False), ''):
portdir = locateportdir(path, wrkdirprefix, False)
if portdir != '':
break
if portdir == '':
raise LocatePDirError(os.path.abspath(path))
wrksrc = querymakevar('WRKSRC', portdir, True)
patchdir = querymakevar('PATCHDIR', portdir, True)
if os.path.isdir(patchdir):
patches.adddir(patchdir, wrksrc)
else:
continue
elif os.path.isfile(path):
portdir = locateportdir(os.path.dirname(path), '' , True)
wrksrc = querymakevar('WRKSRC', portdir, True)
patches.addpatchfile(path, wrksrc)
patch_cookie = querymakevar('PATCH_COOKIE', portdir, True)
if os.path.isfile(patch_cookie):
patch_cookiemtime = os.path.getmtime(patch_cookie)
else:
patch_cookiemtime = 0
for patchobj in patches.getpatchobjs():
write_msg('Updating patchfile: %s...' % \
os.path.basename(patchobj.fullpath))
try:
retval = None
retval = patchobj.update(patch_cookiemtime, \
ignoremtime)
finally:
if retval == None:
write_msg('OUCH!\n')
if retval[0] == False:
write_msg('skipped (%s)\n' % retval[1])
else:
write_msg('ok\n')
if __name__ == '__main__':
try:
main()
except (PatchError, ECmdError, MakeVarError, LocatePDirError) as msg:
sys.exit('ERROR: ' + str(msg))
except IOError as ex:
code, msg = ex
sys.exit('ERROR: %s: %s' % (str(msg), os.strerror(code)))