Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-ports
Path: blob/main/Tools/scripts/patchtool.py
18157 views
1
#!/usr/bin/env python
2
# ex:ts=4
3
#-*- mode: Fundamental; tab-width: 4; -*-
4
#
5
# patchtool.py - a tool to automate common operation with patchfiles in the
6
# FreeBSD Ports Collection.
7
#
8
# ----------------------------------------------------------------------------
9
# "THE BEER-WARE LICENSE" (Revision 42, (c) Poul-Henning Kamp):
10
# Maxim Sobolev <[email protected]> wrote this file. As long as you retain
11
# this notice you can do whatever you want with this stuff. If we meet some
12
# day, and you think this stuff is worth it, you can buy me a beer in return.
13
#
14
# Maxim Sobolev
15
# ----------------------------------------------------------------------------
16
#
17
# MAINTAINER= [email protected] <- any unapproved commits to this file are
18
# highly discouraged!!!
19
#
20
21
import os, os.path, subprocess, sys, getopt, glob, errno, types
22
23
# python3 lacks raw_input
24
compat_raw_input = input
25
if sys.version_info < (3,):
26
compat_raw_input = raw_input
27
28
# Some global variables used as constants
29
#True = 1
30
#False = 0
31
32
def isStr(obj):
33
try:
34
return isinstance(obj, basestring)
35
except NameError:
36
return isinstance(obj, str)
37
38
# Tweakable global variables. User is able to override any of these by setting
39
# appropriate environment variable prefixed by `PT_', eg:
40
# $ export PT_CVS_ID="FooOS"
41
# $ export PT_DIFF_CMD="/usr/local/bin/mydiff"
42
# will force script to use "FooOS" as a CVS_ID instead of "FreeBSD" and
43
# "/usr/local/bin/mydiff" as a command to generate diffs.
44
class Vars:
45
CVS_ID = 'FreeBSD'
46
47
DIFF_ARGS = '-du'
48
DIFF_SUFX = '.orig'
49
PATCH_PREFIX = 'patch-'
50
PATCH_IGN_SUFX = ('.orig', '.rej')
51
RCSDIFF_SUFX = ',v'
52
53
CD_CMD = 'cd'
54
DIFF_CMD = '/usr/bin/diff'
55
MAKE_CMD = '/usr/bin/make'
56
PRINTF_CMD = '/usr/bin/printf'
57
RCSDIFF_CMD = '/usr/bin/rcsdiff'
58
59
DEFAULT_MAKEFILE = 'Makefile'
60
DEV_NULL = '/dev/null'
61
ETC_MAKE_CONF = '/etc/make.conf'
62
63
SLASH_REPL_SYMBOL = '_' # The symbol to replace '/' when auto-generating
64
# patchnames
65
66
67
#
68
# Check if the supplied patch refers to a port's directory.
69
#
70
def isportdir(path, soft = False):
71
REQ_FILES = ('Makefile', 'pkg-descr', 'distinfo')
72
if not os.path.isdir(path) and soft != True:
73
raise IOError(errno.ENOENT, path)
74
# Not reached #
75
76
try:
77
content = os.listdir(path)
78
except OSError:
79
return False
80
81
for file in REQ_FILES:
82
if file not in content:
83
return False
84
return True
85
86
87
#
88
# Traverse directory tree up from the path pointed by argument and return if
89
# root directory of a port is found.
90
#
91
def locateportdir(path, wrkdirprefix= '', strict = False):
92
# Flag to relax error checking in isportdir() function. It required when
93
# WRKDIRPREFIX is defined.
94
softisport = False
95
96
path = os.path.abspath(path)
97
98
if wrkdirprefix != '':
99
wrkdirprefix= os.path.abspath(wrkdirprefix)
100
commonprefix = os.path.commonprefix((path, wrkdirprefix))
101
if commonprefix != wrkdirprefix:
102
return ''
103
path = path[len(wrkdirprefix):]
104
softisport = True
105
106
while path != '/':
107
if isportdir(path, softisport) == True:
108
return path
109
path = os.path.abspath(os.path.join(path, '..'))
110
111
if strict == True:
112
raise LocatePDirError(path)
113
# Not reached #
114
else:
115
return ''
116
117
118
#
119
# Get value of a make(1) variable called varname. Optionally maintain a cache
120
# for resolved varname:makepath pairs to speed-up operation if the same variable
121
# from the exactly same file is requested repeatedly (invocation of make(1) is
122
# very expensive operation...)
123
#
124
def querymakevar(varname, path = 'Makefile', strict = False, cache = {}):
125
path = os.path.abspath(path)
126
127
if (varname, path) in cache:
128
return cache[(varname, path)]
129
130
origpath = path
131
if os.path.isdir(path):
132
path = os.path.join(path, Vars.DEFAULT_MAKEFILE)
133
if not os.path.isfile(path):
134
raise IOError(errno.ENOENT, path)
135
# Not reached #
136
137
dir = os.path.dirname(path)
138
CMDLINE = '%s %s && %s -f %s -V %s' % (Vars.CD_CMD, dir, Vars.MAKE_CMD, \
139
path, varname)
140
devnull = open('/dev/null', 'a')
141
pipe = subprocess.Popen(CMDLINE, shell = True, stdin = subprocess.PIPE, \
142
stdout = subprocess.PIPE, stderr = devnull, close_fds = True)
143
retval = ''
144
for line in pipe.stdout.readlines():
145
retval = retval + line.decode().strip() + ' '
146
retval = retval[:-1]
147
if strict == True and retval.strip() == '':
148
raise MakeVarError(path, varname)
149
# Not reached #
150
151
cache[(varname, origpath)] = retval
152
return retval
153
154
155
#
156
# Get a path of `path' relatively to wrksrc. For example:
157
# path: /foo/bar
158
# wrksrc: /foo/bar/baz/somefile.c
159
# getrelpath: baz/somefile.c
160
# Most of the code here is to handle cases when ../ operation is required to
161
# reach wrksrc from path, for example:
162
# path: /foo/bar
163
# wrksrc: /foo/baz/somefile.c
164
# getrelpath: ../baz/somefile.c
165
#
166
def getrelpath(path, wrksrc):
167
path = os.path.abspath(path)
168
wrksrc = os.path.abspath(wrksrc) + '/'
169
commonpart = os.path.commonprefix((path, wrksrc))
170
while commonpart[-1:] != '/':
171
commonpart = commonpart[:-1]
172
path = path[len(commonpart):]
173
wrksrc = wrksrc[len(commonpart):]
174
adjust = ''
175
while os.path.normpath(os.path.join(wrksrc, adjust)) != '.':
176
adjust = os.path.join(adjust, '..')
177
relpath = os.path.join(adjust, path)
178
return relpath
179
180
181
#
182
# Generate a diff between saved and current versions of the file pointed by the
183
# wrksrc+path. Apply heuristics to locate saved version of the file in question
184
# and if it fails assume that file is new, so /dev/null is to be used as
185
# original file. Optionally save generated patch into `outfile' instead of
186
# dumping it to stdout. Generated patches automatically being tagged with
187
# "FreeBSD" cvs id.
188
#
189
def gendiff(path, wrksrc, outfile = ''):
190
fullpath = os.path.join(wrksrc, path)
191
if not os.path.isfile(fullpath):
192
raise IOError(errno.ENOENT, fullpath)
193
# Not reached #
194
195
cmdline = ''
196
if os.path.isfile(fullpath + Vars.DIFF_SUFX): # Normal diff
197
path_orig = path + Vars.DIFF_SUFX
198
cmdline = '%s %s %s %s' % (Vars.DIFF_CMD, Vars.DIFF_ARGS, path_orig, path)
199
elif os.path.isfile(fullpath + Vars.RCSDIFF_SUFX): # RCS diff
200
path_orig = path
201
cmdline = '%s %s %s' % (Vars.RCSDIFF_CMD, Vars.DIFF_ARGS, path)
202
else: # New file
203
path_orig = Vars.DEV_NULL
204
cmdline = '%s %s %s %s' % (Vars.DIFF_CMD, Vars.DIFF_ARGS, path_orig, path)
205
206
savedir = os.getcwd()
207
os.chdir(wrksrc)
208
devnull = open('/dev/null', 'a')
209
pipe = subprocess.Popen(cmdline, shell = True, stdin = subprocess.PIPE, \
210
stdout = subprocess.PIPE, stderr = devnull, close_fds = True)
211
outbuf = [x.decode() for x in pipe.stdout.readlines()]
212
exitval = pipe.wait()
213
if exitval == 0: # No differences were found
214
retval = False
215
retmsg = 'no differences found between original and current ' \
216
'version of "%s"' % fullpath
217
elif exitval == 1: # Some differences were found
218
if (outfile != ''):
219
outbuf[0] = '--- %s\n' % path_orig
220
outbuf[1] = '+++ %s\n' % path
221
open(outfile, 'w').writelines(outbuf)
222
else:
223
sys.stdout.writelines(outbuf)
224
retval = True
225
retmsg = ''
226
else: # Error occurred
227
raise ECmdError('"%s"' % cmdline, \
228
'external command returned non-zero error code')
229
# Not reached #
230
231
os.chdir(savedir)
232
return (retval, retmsg)
233
234
235
#
236
# Automatically generate a name for a patch based on its path relative to
237
# wrksrc. Use simple scheme to ensure 1-to-1 mapping between path and
238
# patchname - replace all '_' with '__' and all '/' with '_'.
239
#
240
def makepatchname(path, patchdir = ''):
241
SRS = Vars.SLASH_REPL_SYMBOL
242
retval = Vars.PATCH_PREFIX + \
243
path.replace(SRS, SRS + SRS).replace('/', SRS)
244
retval = os.path.join(patchdir, retval)
245
return retval
246
247
248
#
249
# Write a specified message to stderr.
250
#
251
def write_msg(message):
252
if isStr(message):
253
message = message,
254
sys.stderr.writelines(message)
255
256
257
#
258
# Print specified message to stdout and ask user [y/N]?. Optionally allow
259
# specify default answer, i.e. return value if user typed only <cr>
260
#
261
def query_yn(message, default = False):
262
while True:
263
if default == True:
264
yn = 'Y/n'
265
elif default == False:
266
yn = 'y/N'
267
else:
268
yn = 'Y/N'
269
270
reply = compat_raw_input('%s [%s]: ' % (message, yn))
271
272
if reply == 'y' or reply == 'Y':
273
return True
274
elif reply == 'n' or reply == 'N':
275
return False
276
elif reply == '' and default in (True, False):
277
return default
278
print('Wrong answer "%s", please try again' % reply)
279
return default
280
281
282
#
283
# Print optional message and usage information and exit with specified exit
284
# code.
285
#
286
def usage(code, msg = ''):
287
myname = os.path.basename(sys.argv[0])
288
write_msg((str(msg), """
289
Usage: %s [-afi] file ...
290
%s -u [-i] [patchfile|patchdir ...]
291
""" % (myname, myname)))
292
sys.exit(code)
293
294
295
#
296
# Simple custom exception
297
#
298
class MyError(Exception):
299
msg = 'error'
300
301
def __init__(self, file, msg=''):
302
self.file = file
303
if msg != '':
304
self.msg = msg
305
306
def __str__(self):
307
return '%s: %s' % (self.file, self.msg)
308
309
310
#
311
# Error parsing patchfile
312
#
313
class PatchError(MyError):
314
msg = 'corrupt patchfile, or not patchfile at all'
315
316
317
#
318
# Error executing external command
319
#
320
class ECmdError(MyError):
321
pass
322
323
324
#
325
# Error getting value of makefile variable
326
#
327
class MakeVarError(MyError):
328
def __init__(self, file, makevar, msg=''):
329
self.file = file
330
if msg != '':
331
self.msg = msg
332
else:
333
self.msg = 'can\'t get %s value' % makevar
334
335
336
#
337
# Error locating portdir
338
#
339
class LocatePDirError(MyError):
340
msg = 'can\'t locate portdir'
341
342
343
class Patch:
344
fullpath = ''
345
minus3file = ''
346
plus3file = ''
347
wrksrc = ''
348
patchmtime = 0
349
targetmtime = 0
350
351
def __init__(self, path, wrksrc):
352
MINUS3_DELIM = '--- '
353
PLUS3_DELIM = '+++ '
354
355
path = os.path.abspath(path)
356
if not os.path.isfile(path):
357
raise IOError(errno.ENOENT, path)
358
# Not reached #
359
360
self.fullpath = path
361
filedes = open(path)
362
363
for line in filedes.readlines():
364
if self.minus3file == '':
365
if line[:len(MINUS3_DELIM)] == MINUS3_DELIM:
366
lineparts = line.split()
367
try:
368
self.minus3file = lineparts[1]
369
except IndexError:
370
raise PatchError(path)
371
# Not reached #
372
continue
373
elif line[:len(PLUS3_DELIM)] == PLUS3_DELIM:
374
lineparts = line.split()
375
try:
376
self.plus3file = lineparts[1]
377
except IndexError:
378
raise PatchError(path)
379
# Not reached #
380
break
381
382
filedes.close()
383
384
if self.minus3file == '' or self.plus3file == '':
385
raise PatchError(path)
386
# Not reached #
387
388
self.wrksrc = os.path.abspath(wrksrc)
389
self.patchmtime = os.path.getmtime(self.fullpath)
390
plus3file = os.path.join(self.wrksrc, self.plus3file)
391
if os.path.isfile(plus3file):
392
self.targetmtime = os.path.getmtime(plus3file)
393
else:
394
self.targetmtime = 0
395
396
def update(self, patch_cookiemtime = 0, ignoremtime = False):
397
targetfile = os.path.join(self.wrksrc, self.plus3file)
398
if not os.path.isfile(targetfile):
399
raise IOError(errno.ENOENT, targetfile)
400
# Not reached #
401
402
patchdir = os.path.dirname(self.fullpath)
403
if not os.path.isdir(patchdir):
404
os.mkdir(patchdir)
405
406
if ignoremtime == True or self.patchmtime == 0 or \
407
self.targetmtime == 0 or \
408
(self.patchmtime < self.targetmtime and \
409
patch_cookiemtime < self.targetmtime):
410
retval = gendiff(self.plus3file, self.wrksrc, self.fullpath)
411
if retval[0] == True:
412
self.patchmtime = os.path.getmtime(self.fullpath)
413
else:
414
retval = (False, 'patch is already up to date')
415
return retval
416
417
418
class NewPatch(Patch):
419
def __init__(self, patchdir, wrksrc, relpath):
420
self.fullpath = makepatchname(relpath, os.path.abspath(patchdir))
421
self.wrksrc = os.path.abspath(wrksrc)
422
self.plus3file = relpath
423
self.minus3file = relpath
424
self.patchmtime = 0
425
plus3file = os.path.join(self.wrksrc, self.plus3file)
426
if os.path.isfile(plus3file):
427
self.targetmtime = os.path.getmtime(plus3file)
428
else:
429
self.targetmtime = 0
430
431
432
class PatchesCollection:
433
patches = {}
434
435
def __init__(self):
436
self.patches = {}
437
pass
438
439
def adddir(self, patchdir, wrksrc):
440
if not os.path.isdir(patchdir):
441
raise IOError(errno.ENOENT, patchdir)
442
# Not reached #
443
444
for filename in glob.glob(os.path.join(patchdir, Vars.PATCH_PREFIX + '*')):
445
for sufx in Vars.PATCH_IGN_SUFX:
446
if filename[-len(sufx):] == sufx:
447
write_msg('WARNING: patchfile "%s" ignored\n' % filename)
448
break
449
else:
450
self.addpatchfile(filename, wrksrc)
451
452
def addpatchfile(self, path, wrksrc):
453
path = os.path.abspath(path)
454
if path not in self.patches:
455
self.addpatchobj(Patch(path, wrksrc))
456
457
def addpatchobj(self, patchobj):
458
self.patches[patchobj.fullpath] = patchobj
459
460
def lookupbyname(self, path):
461
path = os.path.abspath(path)
462
if path in self.patches:
463
return self.patches[path]
464
return None
465
466
def lookupbytarget(self, wrksrc, relpath):
467
wrksrc = os.path.abspath(wrksrc)
468
for patch in self.patches.values():
469
if wrksrc == patch.wrksrc and relpath == patch.plus3file:
470
return patch
471
return None
472
473
def getpatchobjs(self):
474
return self.patches.values()
475
476
477
#
478
# Resolve all symbolic links in the given path to a file
479
#
480
def truepath(path):
481
if not os.path.isfile(path):
482
raise IOError(errno.ENOENT, path)
483
484
result = ''
485
while len(path) > 0:
486
path, lastcomp = os.path.split(path)
487
if len(lastcomp) == 0:
488
lastcomp = path
489
path = ''
490
result = os.path.join(lastcomp, result)
491
if len(path) == 0:
492
break
493
if os.path.islink(path):
494
linkto = os.path.normpath(os.readlink(path))
495
if linkto[0] != '/':
496
path = os.path.join(path, linkto)
497
else:
498
path = linkto
499
return result[:-1]
500
501
502
def main():
503
try:
504
opts, args = getopt.getopt(sys.argv[1:], 'afui')
505
except getopt.GetoptError as msg:
506
usage(2, msg)
507
508
automatic = False
509
force = False
510
mode = generate
511
ignoremtime = False
512
513
for o, a in opts:
514
if o == '-a':
515
automatic = True
516
elif o == '-f':
517
force = True
518
elif o == '-u':
519
mode = update
520
elif o == '-i':
521
ignoremtime = True
522
else:
523
usage(2)
524
525
# Allow user to override internal constants
526
for varname in dir(Vars):
527
if varname[:2] == '__' and varname[-2:] == '__':
528
continue
529
try:
530
value = os.environ['PT_' + varname]
531
setattr(Vars, varname, value)
532
except KeyError:
533
pass
534
535
mode(args, automatic, force, ignoremtime)
536
537
sys.exit(0)
538
539
540
#
541
# Display a diff or generate patchfile for the files pointed out by args.
542
#
543
def generate(args, automatic, force, ignoremtime):
544
if len(args) == 0:
545
usage(2, "ERROR: no input files specified")
546
547
patches = PatchesCollection()
548
549
for filepath in args:
550
for suf in Vars.RCSDIFF_SUFX, Vars.DIFF_SUFX:
551
if filepath.endswith(suf):
552
filepath = filepath[:-len(suf)]
553
break
554
if not os.path.isfile(filepath):
555
raise IOError(errno.ENOENT, filepath)
556
# Not reached #
557
558
filepath = truepath(filepath)
559
560
wrkdirprefix = querymakevar('WRKDIRPREFIX', Vars.ETC_MAKE_CONF, False)
561
portdir = locateportdir(os.path.dirname(filepath), wrkdirprefix, True)
562
wrksrc = querymakevar('WRKSRC', portdir, True)
563
564
relpath = getrelpath(filepath, wrksrc)
565
566
if automatic:
567
patchdir = querymakevar('PATCHDIR', portdir, True)
568
569
if os.path.isdir(patchdir):
570
patches.adddir(patchdir, wrksrc)
571
572
extra_patches = querymakevar('EXTRA_PATCHES', portdir, False)
573
for extra_patch in extra_patches.split():
574
if os.path.isfile(extra_patch):
575
patches.addpatchfile(extra_patch, wrksrc)
576
577
patchobj = patches.lookupbytarget(wrksrc, relpath)
578
if patchobj == None:
579
patchobj = NewPatch(patchdir, wrksrc, relpath)
580
patches.addpatchobj(patchobj)
581
582
if not force and os.path.exists(patchobj.fullpath) and \
583
os.path.getsize(patchobj.fullpath) > 0:
584
try:
585
retval = query_yn('Target patchfile "%s" already ' \
586
'exists, do you want to replace it?' % \
587
os.path.basename(patchobj.fullpath))
588
except KeyboardInterrupt:
589
sys.exit('\nAction aborted')
590
# Not reached #
591
if retval == False:
592
continue
593
594
write_msg('Generating patchfile: %s...' % \
595
os.path.basename(patchobj.fullpath))
596
597
try:
598
retval = None
599
retval = patchobj.update(ignoremtime = ignoremtime)
600
finally:
601
# Following tricky magic intended to let us append \n even if
602
# we are going to die due to unhandled exception
603
if retval == None:
604
write_msg('OUCH!\n')
605
606
if retval[0] == False:
607
write_msg('skipped (%s)\n' % retval[1])
608
else:
609
write_msg('ok\n')
610
611
else: # automatic != True
612
retval = gendiff(relpath, wrksrc)
613
if retval[0] == False:
614
write_msg('WARNING: %s\n' % retval[1])
615
616
617
#
618
# Atomatically update all patches pointed by args (may be individual
619
# patchfiles, patchdirs or any directories in a portdirs). If directory argument
620
# is encountered, all patches that belong to the port are updated. If no
621
# arguments are supplied - current directory is assumed.
622
#
623
# The procedure honours last modification times of the patchfile, file from
624
# which diff to be generated and `EXTRACT_COOKIE' file (usually
625
# ${WRKDIR}/.extract_cookie) to update only those patches that are really need
626
# to be updated.
627
#
628
def update(args, automatic, force, ignoremtime):
629
if len(args) == 0:
630
args = './',
631
632
for path in args:
633
if not os.path.exists(path):
634
raise IOError(errno.ENOENT, path)
635
# Not reached #
636
637
patches = PatchesCollection()
638
639
if os.path.isdir(path):
640
for wrkdirprefix in (querymakevar('WRKDIRPREFIX', \
641
Vars.ETC_MAKE_CONF, False), ''):
642
portdir = locateportdir(path, wrkdirprefix, False)
643
if portdir != '':
644
break
645
if portdir == '':
646
raise LocatePDirError(os.path.abspath(path))
647
# Not reached #
648
649
wrksrc = querymakevar('WRKSRC', portdir, True)
650
patchdir = querymakevar('PATCHDIR', portdir, True)
651
652
if os.path.isdir(patchdir):
653
patches.adddir(patchdir, wrksrc)
654
else:
655
continue
656
657
elif os.path.isfile(path):
658
portdir = locateportdir(os.path.dirname(path), '' , True)
659
wrksrc = querymakevar('WRKSRC', portdir, True)
660
patches.addpatchfile(path, wrksrc)
661
662
patch_cookie = querymakevar('PATCH_COOKIE', portdir, True)
663
if os.path.isfile(patch_cookie):
664
patch_cookiemtime = os.path.getmtime(patch_cookie)
665
else:
666
patch_cookiemtime = 0
667
668
for patchobj in patches.getpatchobjs():
669
write_msg('Updating patchfile: %s...' % \
670
os.path.basename(patchobj.fullpath))
671
672
try:
673
retval = None
674
retval = patchobj.update(patch_cookiemtime, \
675
ignoremtime)
676
finally:
677
if retval == None:
678
write_msg('OUCH!\n')
679
680
if retval[0] == False:
681
write_msg('skipped (%s)\n' % retval[1])
682
else:
683
write_msg('ok\n')
684
685
686
if __name__ == '__main__':
687
try:
688
main()
689
except (PatchError, ECmdError, MakeVarError, LocatePDirError) as msg:
690
sys.exit('ERROR: ' + str(msg))
691
except IOError as ex:
692
code, msg = ex
693
sys.exit('ERROR: %s: %s' % (str(msg), os.strerror(code)))
694
695
696