A new redo-stamp program takes whatever you give it as stdin and uses it to calculate a checksum for the current target. If that checksum is the same as last time, then we consider the target to be unchanged, and we set checked_runid and stamp, but leave changed_runid alone. That will make future callers of redo-ifchange see this target as unmodified. However, this is only "half" support because by the time we run the .do script that calls redo-stamp, it's too late; the caller is a dependant of the stamped program, which is already being rebuilt, even if redo-stamp turns out to say that this target is unchanged. The other half is coming up.
306 lines
11 KiB
Python
306 lines
11 KiB
Python
import sys, os, errno, stat
|
|
import vars, jwack, state
|
|
from helpers import log, log_, debug, debug2, err, warn, unlink, close_on_exec
|
|
|
|
|
|
def _possible_do_files(t):
|
|
t = os.path.join(vars.BASE, t)
|
|
yield "%s.do" % t, t, ''
|
|
dirname,filename = os.path.split(t)
|
|
l = filename.split('.')
|
|
l[0] = os.path.join(dirname, l[0])
|
|
for i in range(1,len(l)+1):
|
|
basename = '.'.join(l[:i])
|
|
ext = '.'.join(l[i:])
|
|
if ext: ext = '.' + ext
|
|
yield (os.path.join(dirname, "default%s.do" % ext),
|
|
os.path.join(dirname, basename), ext)
|
|
|
|
|
|
def _find_do_file(f):
|
|
for dofile,basename,ext in _possible_do_files(f.name):
|
|
debug2('%s: %s ?\n' % (f.name, dofile))
|
|
if os.path.exists(dofile):
|
|
f.add_dep('m', dofile)
|
|
return dofile,basename,ext
|
|
else:
|
|
f.add_dep('c', dofile)
|
|
return None,None,None
|
|
|
|
|
|
def _nice(t):
|
|
return state.relpath(t, vars.STARTDIR)
|
|
|
|
|
|
def _try_stat(filename):
|
|
try:
|
|
return os.stat(filename)
|
|
except OSError, e:
|
|
if e.errno == errno.ENOENT:
|
|
return None
|
|
else:
|
|
raise
|
|
|
|
|
|
def warn_override(name):
|
|
warn('%s - you modified it; skipping\n' % name)
|
|
|
|
|
|
class ImmediateReturn(Exception):
|
|
def __init__(self, rv):
|
|
Exception.__init__(self, "immediate return with exit code %d" % rv)
|
|
self.rv = rv
|
|
|
|
|
|
class BuildJob:
|
|
def __init__(self, t, sf, lock, shouldbuildfunc, donefunc):
|
|
self.t = t # original target name, not relative to vars.BASE
|
|
self.sf = sf
|
|
self.tmpname1 = '%s.redo1.tmp' % t
|
|
self.tmpname2 = '%s.redo2.tmp' % t
|
|
self.lock = lock
|
|
self.shouldbuildfunc = shouldbuildfunc
|
|
self.donefunc = donefunc
|
|
self.before_t = _try_stat(self.t)
|
|
|
|
def start(self):
|
|
assert(self.lock.owned)
|
|
t = self.t
|
|
sf = self.sf
|
|
try:
|
|
if not self.shouldbuildfunc(t):
|
|
# target doesn't need to be built; skip the whole task
|
|
return self._after2(0)
|
|
except ImmediateReturn, e:
|
|
return self._after2(e.rv)
|
|
newstamp = sf.read_stamp()
|
|
if (sf.is_generated and
|
|
not sf.failed_runid and
|
|
newstamp != state.STAMP_MISSING and
|
|
(sf.stamp != newstamp or sf.is_override)):
|
|
warn_override(_nice(t))
|
|
sf.set_override()
|
|
sf.set_checked()
|
|
sf.save()
|
|
return self._after2(0)
|
|
if (os.path.exists(t) and not os.path.exists(t + '/.')
|
|
and not sf.is_generated):
|
|
# an existing source file that was not generated by us.
|
|
# This step is mentioned by djb in his notes.
|
|
# For example, a rule called default.c.do could be used to try
|
|
# to produce hello.c, but we don't want that to happen if
|
|
# hello.c was created by the end user.
|
|
# FIXME: always refuse to redo any file that was modified outside
|
|
# of redo? That would make it easy for someone to override a
|
|
# file temporarily, and could be undone by deleting the file.
|
|
debug2("-- static (%r)\n" % t)
|
|
sf.set_static()
|
|
sf.save()
|
|
return self._after2(0)
|
|
sf.zap_deps()
|
|
(dofile, basename, ext) = _find_do_file(sf)
|
|
if not dofile:
|
|
if os.path.exists(t):
|
|
sf.set_static()
|
|
sf.save()
|
|
return self._after2(0)
|
|
else:
|
|
err('no rule to make %r\n' % t)
|
|
return self._after2(1)
|
|
unlink(self.tmpname1)
|
|
unlink(self.tmpname2)
|
|
ffd = os.open(self.tmpname1, os.O_CREAT|os.O_RDWR|os.O_EXCL, 0666)
|
|
close_on_exec(ffd, True)
|
|
self.f = os.fdopen(ffd, 'w+')
|
|
# this will run in the dofile's directory, so use only basenames here
|
|
argv = ['sh', '-e',
|
|
os.path.basename(dofile),
|
|
os.path.basename(basename), # target name (extension removed)
|
|
ext, # extension (if any), including leading dot
|
|
os.path.basename(self.tmpname2) # randomized output file name
|
|
]
|
|
if vars.VERBOSE: argv[1] += 'v'
|
|
if vars.XTRACE: argv[1] += 'x'
|
|
if vars.VERBOSE or vars.XTRACE: log_('\n')
|
|
log('%s\n' % _nice(t))
|
|
self.argv = argv
|
|
sf.is_generated = True
|
|
sf.save()
|
|
dof = state.File(name=dofile)
|
|
dof.set_static()
|
|
dof.save()
|
|
state.commit()
|
|
jwack.start_job(t, self._do_subproc, self._after)
|
|
|
|
def _do_subproc(self):
|
|
# careful: REDO_PWD was the PWD relative to the STARTPATH at the time
|
|
# we *started* building the current target; but that target ran
|
|
# redo-ifchange, and it might have done it from a different directory
|
|
# than we started it in. So os.getcwd() might be != REDO_PWD right
|
|
# now.
|
|
dn = os.path.dirname(self.t)
|
|
newp = os.path.realpath(dn)
|
|
os.environ['REDO_PWD'] = state.relpath(newp, vars.STARTDIR)
|
|
os.environ['REDO_TARGET'] = os.path.basename(self.t)
|
|
os.environ['REDO_DEPTH'] = vars.DEPTH + ' '
|
|
if dn:
|
|
os.chdir(dn)
|
|
os.dup2(self.f.fileno(), 1)
|
|
os.close(self.f.fileno())
|
|
close_on_exec(1, False)
|
|
if vars.VERBOSE or vars.XTRACE: log_('* %s\n' % ' '.join(self.argv))
|
|
os.execvp(self.argv[0], self.argv)
|
|
assert(0)
|
|
# returns only if there's an exception
|
|
|
|
def _after(self, t, rv):
|
|
try:
|
|
state.check_sane()
|
|
rv = self._after1(t, rv)
|
|
state.commit()
|
|
finally:
|
|
self._after2(rv)
|
|
|
|
def _after1(self, t, rv):
|
|
f = self.f
|
|
before_t = self.before_t
|
|
after_t = _try_stat(t)
|
|
st1 = os.fstat(f.fileno())
|
|
st2 = _try_stat(self.tmpname2)
|
|
if after_t != before_t and not stat.S_ISDIR(after_t.st_mode):
|
|
err('%s modified %s directly!\n' % (self.argv[2], t))
|
|
err('...you should update $3 (a temp file) or stdout, not $1.\n')
|
|
rv = 206
|
|
elif st2 and st1.st_size > 0:
|
|
err('%s wrote to stdout *and* created $3.\n' % self.argv[2])
|
|
err('...you should write status messages to stderr, not stdout.\n')
|
|
rv = 207
|
|
if rv==0:
|
|
if st2:
|
|
os.rename(self.tmpname2, t)
|
|
os.unlink(self.tmpname1)
|
|
elif st1.st_size > 0:
|
|
os.rename(self.tmpname1, t)
|
|
if st2:
|
|
os.unlink(self.tmpname2)
|
|
else: # no output generated at all; that's ok
|
|
unlink(self.tmpname1)
|
|
unlink(t)
|
|
sf = self.sf
|
|
sf.refresh()
|
|
sf.is_generated = True
|
|
sf.is_override = False
|
|
if sf.is_checked() or sf.is_changed():
|
|
# it got checked during the run; someone ran redo-stamp.
|
|
# update_stamp would call set_changed(); we don't want that
|
|
sf.stamp = sf.read_stamp()
|
|
else:
|
|
sf.csum = None
|
|
sf.update_stamp()
|
|
sf.set_changed()
|
|
sf.save()
|
|
else:
|
|
unlink(self.tmpname1)
|
|
unlink(self.tmpname2)
|
|
sf = self.sf
|
|
sf.set_failed()
|
|
sf.save()
|
|
f.close()
|
|
if rv != 0:
|
|
err('%s: exit code %d\n' % (_nice(t),rv))
|
|
else:
|
|
if vars.VERBOSE or vars.XTRACE:
|
|
log('%s (done)\n\n' % _nice(t))
|
|
return rv
|
|
|
|
def _after2(self, rv):
|
|
try:
|
|
self.donefunc(self.t, rv)
|
|
assert(self.lock.owned)
|
|
finally:
|
|
self.lock.unlock()
|
|
|
|
|
|
def main(targets, shouldbuildfunc):
|
|
retcode = [0] # a list so that it can be reassigned from done()
|
|
if vars.SHUFFLE:
|
|
import random
|
|
random.shuffle(targets)
|
|
|
|
locked = []
|
|
|
|
def done(t, rv):
|
|
if rv:
|
|
retcode[0] = 1
|
|
|
|
for i in range(len(targets)):
|
|
t = targets[i]
|
|
|
|
# In the first cycle, we just build as much as we can without worrying
|
|
# about any lock contention. If someone else has it locked, we move on.
|
|
for t in targets:
|
|
if not jwack.has_token():
|
|
state.commit()
|
|
jwack.get_token(t)
|
|
if retcode[0] and not vars.KEEP_GOING:
|
|
break
|
|
if not state.check_sane():
|
|
err('.redo directory disappeared; cannot continue.\n')
|
|
retcode[0] = 205
|
|
break
|
|
f = state.File(name=t)
|
|
lock = state.Lock(f.id)
|
|
lock.trylock()
|
|
if not lock.owned:
|
|
if vars.DEBUG_LOCKS:
|
|
log('%s (locked...)\n' % _nice(t))
|
|
locked.append((f.id,t))
|
|
else:
|
|
BuildJob(t, f, lock, shouldbuildfunc, done).start()
|
|
|
|
# Now we've built all the "easy" ones. Go back and just wait on the
|
|
# remaining ones one by one. There's no reason to do it any more
|
|
# efficiently, because if these targets were previously locked, that
|
|
# means someone else was building them; thus, we probably won't need to
|
|
# do anything. The only exception is if we're invoked as redo instead
|
|
# of redo-ifchange; then we have to redo it even if someone else already
|
|
# did. But that should be rare.
|
|
while locked or jwack.running():
|
|
state.commit()
|
|
jwack.wait_all()
|
|
# at this point, we don't have any children holding any tokens, so
|
|
# it's okay to block below.
|
|
if retcode[0] and not vars.KEEP_GOING:
|
|
break
|
|
if locked:
|
|
if not state.check_sane():
|
|
err('.redo directory disappeared; cannot continue.\n')
|
|
retcode[0] = 205
|
|
break
|
|
fid,t = locked.pop(0)
|
|
lock = state.Lock(fid)
|
|
lock.trylock()
|
|
while not lock.owned:
|
|
if vars.DEBUG_LOCKS:
|
|
warn('%s (WAITING)\n' % _nice(t))
|
|
# this sequence looks a little silly, but the idea is to
|
|
# give up our personal token while we wait for the lock to
|
|
# be released; but we should never run get_token() while
|
|
# holding a lock, or we could cause deadlocks.
|
|
jwack.release_mine()
|
|
lock.waitlock()
|
|
lock.unlock()
|
|
jwack.get_token(t)
|
|
lock.trylock()
|
|
assert(lock.owned)
|
|
if vars.DEBUG_LOCKS:
|
|
log('%s (...unlocked!)\n' % _nice(t))
|
|
if state.File(name=t).is_failed():
|
|
err('%s: failed in another thread\n' % _nice(t))
|
|
retcode[0] = 2
|
|
lock.unlock()
|
|
else:
|
|
BuildJob(t, state.File(id=fid), lock,
|
|
shouldbuildfunc, done).start()
|
|
state.commit()
|
|
return retcode[0]
|