This encourages greater randomness in builds, which both decreases lock contention and should help with testing the consistency of parallel builds.
200 lines
5.4 KiB
Python
Executable file
200 lines
5.4 KiB
Python
Executable file
#!/usr/bin/python
|
|
import sys, os, subprocess, glob, time, random
|
|
import options, jwack
|
|
|
|
optspec = """
|
|
redo [targets...]
|
|
--
|
|
j,jobs= maximum number of jobs to build at once
|
|
d,debug print dependency checks as they happen
|
|
v,verbose print commands as they are run
|
|
"""
|
|
o = options.Options('redo', optspec)
|
|
(opt, flags, extra) = o.parse(sys.argv[1:])
|
|
|
|
targets = extra or ['all']
|
|
|
|
if opt.debug:
|
|
os.environ['REDO_DEBUG'] = '1'
|
|
if opt.verbose:
|
|
os.environ['REDO_VERBOSE'] = '1'
|
|
|
|
if not os.environ.get('REDO_BASE', ''):
|
|
base = os.path.commonprefix([os.path.abspath(os.path.dirname(t))
|
|
for t in targets] + [os.getcwd()])
|
|
bsplit = base.split('/')
|
|
for i in range(len(bsplit)-1, 0, -1):
|
|
newbase = '%s/.redo' % '/'.join(bsplit[:i])
|
|
if os.path.exists(newbase):
|
|
base = newbase
|
|
break
|
|
os.environ['REDO_BASE'] = base
|
|
os.environ['REDO_STARTDIR'] = os.getcwd()
|
|
|
|
# FIXME: just wiping out all the locks is kind of cheating. But we
|
|
# only do this from the toplevel redo process, so unless the user
|
|
# deliberately starts more than one redo on the same repository, it's
|
|
# sort of ok.
|
|
for f in glob.glob('%s/.redo/lock^*' % base):
|
|
os.unlink(f)
|
|
|
|
import vars
|
|
from helpers import *
|
|
|
|
|
|
class BuildError(Exception):
|
|
pass
|
|
class BuildLocked(Exception):
|
|
pass
|
|
|
|
|
|
def find_do_file(t):
|
|
dofile = '%s.do' % t
|
|
if os.path.exists(dofile):
|
|
add_dep(t, 'm', dofile)
|
|
return dofile
|
|
else:
|
|
add_dep(t, 'c', dofile)
|
|
return None
|
|
|
|
|
|
def stamp(t):
|
|
stampfile = sname('stamp', t)
|
|
depfile = sname('dep', t)
|
|
if not os.path.exists(vars.BASE + '/.redo'):
|
|
# .redo might not exist in a 'make clean' target
|
|
return
|
|
open(stampfile, 'w').close()
|
|
open(depfile, 'a').close()
|
|
try:
|
|
mtime = os.stat(t).st_mtime
|
|
except OSError:
|
|
mtime = 0
|
|
os.utime(stampfile, (mtime, mtime))
|
|
|
|
|
|
def _preexec(t):
|
|
os.environ['REDO_TARGET'] = t
|
|
os.environ['REDO_DEPTH'] = vars.DEPTH + ' '
|
|
dn = os.path.dirname(t)
|
|
if dn:
|
|
os.chdir(dn)
|
|
|
|
|
|
def _build(t):
|
|
unlink(sname('dep', t))
|
|
open(sname('dep', t), 'w').close()
|
|
dofile = find_do_file(t)
|
|
if not dofile:
|
|
if os.path.exists(t): # an existing source file
|
|
stamp(t)
|
|
return # success
|
|
else:
|
|
raise BuildError('no rule to make %r' % t)
|
|
stamp(dofile)
|
|
unlink(t)
|
|
tmpname = '%s.redo.tmp' % t
|
|
unlink(tmpname)
|
|
f = open(tmpname, 'w+')
|
|
argv = [os.environ.get('SHELL', 'sh'), '-e',
|
|
os.path.basename(dofile),
|
|
os.path.basename(t), 'FIXME', os.path.basename(tmpname)]
|
|
if vars.VERBOSE:
|
|
argv[1] += 'v'
|
|
log('%s\n' % relpath(t, vars.STARTDIR))
|
|
rv = subprocess.call(argv, preexec_fn=lambda: _preexec(t),
|
|
stdout=f.fileno())
|
|
st = os.stat(tmpname)
|
|
stampfile = sname('stamp', t)
|
|
if rv==0:
|
|
if st.st_size:
|
|
os.rename(tmpname, t)
|
|
else:
|
|
unlink(tmpname)
|
|
stamp(t)
|
|
else:
|
|
unlink(tmpname)
|
|
unlink(stampfile)
|
|
f.close()
|
|
if rv != 0:
|
|
raise BuildError('%s: exit code %d' % (t,rv))
|
|
|
|
|
|
def build(t):
|
|
mkdirp('%s/.redo' % vars.BASE)
|
|
lockname = sname('lock', t)
|
|
try:
|
|
fd = os.open(lockname, os.O_CREAT|os.O_EXCL)
|
|
except OSError, e:
|
|
if e.errno == errno.EEXIST:
|
|
log('%s (locked...)\n' % relpath(t, vars.STARTDIR))
|
|
os._exit(199)
|
|
else:
|
|
raise
|
|
os.close(fd)
|
|
try:
|
|
try:
|
|
return _build(t)
|
|
except BuildError, e:
|
|
err('%s\n' % e)
|
|
os._exit(1)
|
|
finally:
|
|
unlink(lockname)
|
|
|
|
|
|
def main():
|
|
retcode = 0
|
|
locked = {}
|
|
waits = {}
|
|
random.shuffle(targets) # make it unpredictable for better testing
|
|
for t in targets:
|
|
if os.path.exists('%s/all.do' % t):
|
|
# t is a directory, but it has a default target
|
|
t = '%s/all' % t
|
|
waits[t] = jwack.start_job(t, lambda: build(t))
|
|
jwack.wait_all()
|
|
for t,pd in waits.items():
|
|
assert(pd.rv != None)
|
|
if pd.rv == 199:
|
|
# target was locked
|
|
locked[t] = 1
|
|
elif pd.rv:
|
|
err('%s: exit code was %r\n' % (t, pd.rv))
|
|
retcode = 1
|
|
while locked:
|
|
for t in locked.keys():
|
|
lockname = sname('lock', t)
|
|
stampname = sname('stamp', t)
|
|
if not os.path.exists(lockname):
|
|
relp = relpath(t, vars.STARTDIR)
|
|
log('%s (...unlocked!)\n' % relp)
|
|
if not os.path.exists(stampname):
|
|
err('%s: failed in another thread\n' % relp)
|
|
retcode = 2
|
|
del locked[t]
|
|
time.sleep(0.2)
|
|
return retcode
|
|
|
|
|
|
if not vars.DEPTH:
|
|
# toplevel call to redo
|
|
exenames = [os.path.abspath(sys.argv[0]), os.path.realpath(sys.argv[0])]
|
|
if exenames[0] == exenames[1]:
|
|
exenames = [exenames[0]]
|
|
dirnames = [os.path.dirname(p) for p in exenames]
|
|
os.environ['PATH'] = ':'.join(dirnames) + ':' + os.environ['PATH']
|
|
|
|
try:
|
|
j = atoi(opt.jobs or 1)
|
|
if j < 1 or j > 1000:
|
|
err('invalid --jobs value: %r\n' % opt.jobs)
|
|
jwack.setup(j)
|
|
try:
|
|
retcode = main()
|
|
finally:
|
|
jwack.force_return_tokens()
|
|
if retcode:
|
|
err('exiting: %d\n' % retcode)
|
|
sys.exit(retcode)
|
|
except KeyboardInterrupt:
|
|
sys.exit(200)
|