Merge branch 'sqlite'

This replaces the .redo state directory with an sqlite database instead,
improving correctness and sometimes performance.
This commit is contained in:
Avery Pennarun 2010-12-10 05:43:43 -08:00
commit b1bb48a029
15 changed files with 385 additions and 277 deletions

View file

@ -1,9 +1,10 @@
import sys, os, random, errno, stat import sys, os, errno, stat
import vars, jwack, state import vars, jwack, state
from helpers import log, log_, debug2, err, unlink, close_on_exec from helpers import log, log_, debug2, err, warn, unlink, close_on_exec
def _possible_do_files(t): def _possible_do_files(t):
t = os.path.join(vars.BASE, t)
yield "%s.do" % t, t, '' yield "%s.do" % t, t, ''
dirname,filename = os.path.split(t) dirname,filename = os.path.split(t)
l = filename.split('.') l = filename.split('.')
@ -16,14 +17,14 @@ def _possible_do_files(t):
os.path.join(dirname, basename), ext) os.path.join(dirname, basename), ext)
def _find_do_file(t): def _find_do_file(f):
for dofile,basename,ext in _possible_do_files(t): for dofile,basename,ext in _possible_do_files(f.name):
debug2('%s: %s ?\n' % (t, dofile)) debug2('%s: %s ?\n' % (f.name, dofile))
if os.path.exists(dofile): if os.path.exists(dofile):
state.add_dep(t, 'm', dofile) f.add_dep('m', dofile)
return dofile,basename,ext return dofile,basename,ext
else: else:
state.add_dep(t, 'c', dofile) f.add_dep('c', dofile)
return None,None,None return None,None,None
@ -42,8 +43,9 @@ def _try_stat(filename):
class BuildJob: class BuildJob:
def __init__(self, t, lock, shouldbuildfunc, donefunc): def __init__(self, t, sf, lock, shouldbuildfunc, donefunc):
self.t = t self.t = t # original target name, not relative to vars.BASE
self.sf = sf
self.tmpname = '%s.redo.tmp' % t self.tmpname = '%s.redo.tmp' % t
self.lock = lock self.lock = lock
self.shouldbuildfunc = shouldbuildfunc self.shouldbuildfunc = shouldbuildfunc
@ -53,12 +55,13 @@ class BuildJob:
def start(self): def start(self):
assert(self.lock.owned) assert(self.lock.owned)
t = self.t t = self.t
sf = self.sf
tmpname = self.tmpname tmpname = self.tmpname
if not self.shouldbuildfunc(t): if not self.shouldbuildfunc(t):
# target doesn't need to be built; skip the whole task # target doesn't need to be built; skip the whole task
return self._after2(0) return self._after2(0)
if (os.path.exists(t) and not os.path.exists(t + '/.') if (os.path.exists(t) and not os.path.exists(t + '/.')
and not state.is_generated(t)): and not sf.is_generated):
# an existing source file that was not generated by us. # an existing source file that was not generated by us.
# This step is mentioned by djb in his notes. # This step is mentioned by djb in his notes.
# For example, a rule called default.c.do could be used to try # For example, a rule called default.c.do could be used to try
@ -67,20 +70,21 @@ class BuildJob:
# FIXME: always refuse to redo any file that was modified outside # FIXME: always refuse to redo any file that was modified outside
# of redo? That would make it easy for someone to override a # of redo? That would make it easy for someone to override a
# file temporarily, and could be undone by deleting the file. # file temporarily, and could be undone by deleting the file.
state.unmark_as_generated(t) debug2("-- static (%r)\n" % t)
state.stamp_and_maybe_built(t) sf.set_static()
sf.save()
return self._after2(0) return self._after2(0)
state.start(t) sf.zap_deps()
(dofile, basename, ext) = _find_do_file(t) (dofile, basename, ext) = _find_do_file(sf)
if not dofile: if not dofile:
if os.path.exists(t): if os.path.exists(t):
state.unmark_as_generated(t) sf.is_generated = False
state.stamp_and_maybe_built(t) sf.set_static()
sf.save()
return self._after2(0) return self._after2(0)
else: else:
err('no rule to make %r\n' % t) err('no rule to make %r\n' % t)
return self._after2(1) return self._after2(1)
state.stamp_and_maybe_built(dofile)
unlink(tmpname) unlink(tmpname)
ffd = os.open(tmpname, os.O_CREAT|os.O_RDWR|os.O_EXCL, 0666) ffd = os.open(tmpname, os.O_CREAT|os.O_RDWR|os.O_EXCL, 0666)
close_on_exec(ffd, True) close_on_exec(ffd, True)
@ -97,13 +101,20 @@ class BuildJob:
if vars.VERBOSE or vars.XTRACE: log_('\n') if vars.VERBOSE or vars.XTRACE: log_('\n')
log('%s\n' % _nice(t)) log('%s\n' % _nice(t))
self.argv = argv 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) jwack.start_job(t, self._do_subproc, self._after)
def _do_subproc(self): def _do_subproc(self):
# careful: REDO_PWD was the PWD relative to the STARTPATH at the time # careful: REDO_PWD was the PWD relative to the STARTPATH at the time
# we *started* building the current target; but that target ran # we *started* building the current target; but that target ran
# redo-ifchange, and it might have done it from a different directory # 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. # than we started it in. So os.getcwd() might be != REDO_PWD right
# now.
dn = os.path.dirname(self.t) dn = os.path.dirname(self.t)
newp = os.path.realpath(dn) newp = os.path.realpath(dn)
os.environ['REDO_PWD'] = state.relpath(newp, vars.STARTDIR) os.environ['REDO_PWD'] = state.relpath(newp, vars.STARTDIR)
@ -121,7 +132,9 @@ class BuildJob:
def _after(self, t, rv): def _after(self, t, rv):
try: try:
state.check_sane()
rv = self._after1(t, rv) rv = self._after1(t, rv)
state.commit()
finally: finally:
self._after2(rv) self._after2(rv)
@ -153,11 +166,17 @@ class BuildJob:
os.rename(tmpname, t) os.rename(tmpname, t)
else: else:
unlink(tmpname) unlink(tmpname)
state.built(t) sf = self.sf
state.stamp(t) sf.is_generated=True
sf.update_stamp()
sf.set_changed()
sf.save()
else: else:
unlink(tmpname) unlink(tmpname)
state.unstamp(t) sf = self.sf
sf.stamp = None
sf.set_changed()
sf.save()
f.close() f.close()
if rv != 0: if rv != 0:
err('%s: exit code %d\n' % (_nice(t),rv)) err('%s: exit code %d\n' % (_nice(t),rv))
@ -177,6 +196,7 @@ class BuildJob:
def main(targets, shouldbuildfunc): def main(targets, shouldbuildfunc):
retcode = [0] # a list so that it can be reassigned from done() retcode = [0] # a list so that it can be reassigned from done()
if vars.SHUFFLE: if vars.SHUFFLE:
import random
random.shuffle(targets) random.shuffle(targets)
locked = [] locked = []
@ -191,45 +211,60 @@ def main(targets, shouldbuildfunc):
# In the first cycle, we just build as much as we can without worrying # 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. # about any lock contention. If someone else has it locked, we move on.
for t in targets: for t in targets:
if not jwack.has_token():
state.commit()
jwack.get_token(t) jwack.get_token(t)
if retcode[0] and not vars.KEEP_GOING: if retcode[0] and not vars.KEEP_GOING:
break break
if not state.is_sane(): if not state.check_sane():
err('.redo directory disappeared; cannot continue.\n')
retcode[0] = 205 retcode[0] = 205
break break
lock = state.Lock(t) f = state.File(name=t)
lock = state.Lock(f.id)
lock.trylock() lock.trylock()
if not lock.owned: if not lock.owned:
if vars.DEBUG_LOCKS: if vars.DEBUG_LOCKS:
log('%s (locked...)\n' % _nice(t)) log('%s (locked...)\n' % _nice(t))
locked.append(t) locked.append((f.id,t))
else: else:
BuildJob(t, lock, shouldbuildfunc, done).start() BuildJob(t, f, lock, shouldbuildfunc, done).start()
# Now we've built all the "easy" ones. Go back and just wait on the # Now we've built all the "easy" ones. Go back and just wait on the
# remaining ones one by one. This is technically non-optimal; we could # remaining ones one by one. There's no reason to do it any more
# use select.select() to wait on more than one at a time. But it should # efficiently, because if these targets were previously locked, that
# be rare enough that it doesn't matter, and the logic is easier this way. # 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(): while locked or jwack.running():
state.commit()
jwack.wait_all() jwack.wait_all()
# at this point, we don't have any children holding any tokens, so # at this point, we don't have any children holding any tokens, so
# it's okay to block below. # it's okay to block below.
if retcode[0] and not vars.KEEP_GOING: if retcode[0] and not vars.KEEP_GOING:
break break
if locked: if locked:
if not state.is_sane(): if not state.check_sane():
err('.redo directory disappeared; cannot continue.\n')
retcode[0] = 205 retcode[0] = 205
break break
t = locked.pop(0) fid,t = locked.pop(0)
lock = state.Lock(t) lock = state.Lock(fid)
lock.trylock()
if not lock.owned:
if vars.DEBUG_LOCKS and len(locked) >= 1:
warn('%s (WAITING)\n' % _nice(t))
lock.waitlock() lock.waitlock()
assert(lock.owned) assert(lock.owned)
if vars.DEBUG_LOCKS: if vars.DEBUG_LOCKS:
log('%s (...unlocked!)\n' % _nice(t)) log('%s (...unlocked!)\n' % _nice(t))
if state.stamped(t) == None: if state.File(name=t).stamp == None:
err('%s: failed in another thread\n' % _nice(t)) err('%s: failed in another thread\n' % _nice(t))
retcode[0] = 2 retcode[0] = 2
lock.unlock() lock.unlock()
else: else:
BuildJob(t, lock, shouldbuildfunc, done).start() BuildJob(t, state.File(id=fid), lock,
shouldbuildfunc, done).start()
state.commit()
return retcode[0] return retcode[0]

View file

@ -15,24 +15,6 @@ def unlink(f):
pass # it doesn't exist, that's what you asked for pass # it doesn't exist, that's what you asked for
def mkdirp(d, mode=None):
"""Recursively create directories on path 'd'.
Unlike os.makedirs(), it doesn't raise an exception if the last element of
the path already exists.
"""
try:
if mode:
os.makedirs(d, mode)
else:
os.makedirs(d)
except OSError, e:
if e.errno == errno.EEXIST:
pass
else:
raise
def log_(s): def log_(s):
sys.stdout.flush() sys.stdout.flush()
if vars.DEBUG_PIDS: if vars.DEBUG_PIDS:
@ -52,13 +34,20 @@ def _cerr(s):
def _bwerr(s): def _bwerr(s):
log_('redo: %s%s' % (vars.DEPTH, s)) log_('redo: %s%s' % (vars.DEPTH, s))
def _cwarn(s):
log_('\x1b[33mredo: %s\x1b[1m%s\x1b[m' % (vars.DEPTH, s))
def _bwwarn(s):
log_('redo: %s%s' % (vars.DEPTH, s))
if os.isatty(2): if os.isatty(2):
log = _clog log = _clog
err = _cerr err = _cerr
warn = _cwarn
else: else:
log = _bwlog log = _bwlog
err = _bwerr err = _bwerr
warn = _bwwarn
def debug(s): def debug(s):

View file

@ -1,7 +1,7 @@
# #
# beware the jobberwack # beware the jobberwack
# #
import sys, os, errno, select, fcntl import sys, os, errno, select, fcntl, signal
import atoi import atoi
_toplevel = 0 _toplevel = 0
@ -24,22 +24,35 @@ def _release(n):
_mytokens = 1 _mytokens = 1
def _timeout(sig, frame):
pass
def _try_read(fd, n): def _try_read(fd, n):
# FIXME: this isn't actually safe, because GNU make can't handle it if # using djb's suggested way of doing non-blocking reads from a blocking
# the socket is nonblocking. Ugh. That means we'll have to do their # socket: http://cr.yp.to/unix/nonblock.html
# horrible SIGCHLD hack after all. # We can't just make the socket non-blocking, because we want to be
fcntl.fcntl(_fds[0], fcntl.F_SETFL, os.O_NONBLOCK) # compatible with GNU Make, and they can't handle it.
r,w,x = select.select([fd], [], [], 0)
if not r:
return '' # try again
# ok, the socket is readable - but some other process might get there
# first. We have to set an alarm() in case our read() gets stuck.
oldh = signal.signal(signal.SIGALRM, _timeout)
try: try:
signal.alarm(1) # emergency fallback
try: try:
b = os.read(_fds[0], 1) b = os.read(_fds[0], 1)
except OSError, e: except OSError, e:
if e.errno == errno.EAGAIN: if e.errno in (errno.EAGAIN, errno.EINTR):
return '' # interrupted or it was nonblocking
return '' # try again
else: else:
raise raise
finally: finally:
fcntl.fcntl(_fds[0], fcntl.F_SETFL, 0) signal.alarm(0)
return b and b or None signal.signal(signal.SIGALRM, oldh)
return b and b or None # None means EOF
def setup(maxjobs): def setup(maxjobs):
@ -70,7 +83,11 @@ def setup(maxjobs):
if maxjobs and not _fds: if maxjobs and not _fds:
# need to start a new server # need to start a new server
_toplevel = maxjobs _toplevel = maxjobs
_fds = os.pipe() _fds1 = os.pipe()
_fds = (fcntl.fcntl(_fds1[0], fcntl.F_DUPFD, 100),
fcntl.fcntl(_fds1[1], fcntl.F_DUPFD, 101))
os.close(_fds1[0])
os.close(_fds1[1])
_release(maxjobs-1) _release(maxjobs-1)
os.putenv('MAKEFLAGS', os.putenv('MAKEFLAGS',
'%s --jobserver-fds=%d,%d -j' % (os.getenv('MAKEFLAGS'), '%s --jobserver-fds=%d,%d -j' % (os.getenv('MAKEFLAGS'),
@ -105,6 +122,11 @@ def wait(want_token):
pd.donefunc(pd.name, pd.rv) pd.donefunc(pd.name, pd.rv)
def has_token():
if _mytokens >= 1:
return True
def get_token(reason): def get_token(reason):
global _mytokens global _mytokens
assert(_mytokens <= 1) assert(_mytokens <= 1)
@ -149,8 +171,8 @@ def wait_all():
bb += b bb += b
if not b: break if not b: break
if len(bb) != _toplevel-1: if len(bb) != _toplevel-1:
raise Exception('on exit: expected %d tokens; found only %d' raise Exception('on exit: expected %d tokens; found only %r'
% (_toplevel-1, len(b))) % (_toplevel-1, len(bb)))
os.write(_fds[1], bb) os.write(_fds[1], bb)

View file

@ -1,61 +1,61 @@
#!/usr/bin/python #!/usr/bin/python
import sys, os, errno, stat import sys, os, errno, stat
import vars, state, builder, jwack import vars, state, builder, jwack
from helpers import debug, debug2, err, mkdirp, unlink from helpers import debug, debug2, err, unlink
def dirty_deps(t, depth): def dirty_deps(f, depth, max_changed):
try: if vars.DEBUG >= 1: debug('%s?%s\n' % (depth, f.name))
st = os.stat(t)
realtime = st.st_mtime
except OSError:
st = None
realtime = 0
debug('%s?%s\n' % (depth, t)) if f.changed_runid == None:
if state.isbuilt(t): debug('%s-- DIRTY (never built)\n' % depth)
return True
if f.changed_runid > max_changed:
debug('%s-- DIRTY (built)\n' % depth) debug('%s-- DIRTY (built)\n' % depth)
return True # has already been built during this session return True # has been built more recently than parent
if state.ismarked(t): if f.is_checked():
debug('%s-- CLEAN (marked)\n' % depth) if vars.DEBUG >= 1: debug('%s-- CLEAN (checked)\n' % depth)
return False # has already been checked during this session return False # has already been checked during this session
stamptime = state.stamped(t) if not f.stamp:
if stamptime == None:
debug('%s-- DIRTY (no stamp)\n' % depth) debug('%s-- DIRTY (no stamp)\n' % depth)
return True return True
if stamptime != realtime and not (st and stat.S_ISDIR(st.st_mode)): if f.stamp != f.read_stamp():
debug('%s-- DIRTY (mtime)\n' % depth) debug('%s-- DIRTY (mtime)\n' % depth)
return True return True
for mode,name in state.deps(t): for mode,f2 in f.deps():
if mode == 'c': if mode == 'c':
if os.path.exists(name): if os.path.exists(os.path.join(vars.BASE, f2.name)):
debug('%s-- DIRTY (created)\n' % depth) debug('%s-- DIRTY (created)\n' % depth)
return True return True
elif mode == 'm': elif mode == 'm':
if dirty_deps(os.path.join(vars.BASE, name), depth + ' '): if dirty_deps(f2, depth = depth + ' ',
max_changed = f.changed_runid):
debug('%s-- DIRTY (sub)\n' % depth) debug('%s-- DIRTY (sub)\n' % depth)
state.unstamp(t) # optimization for future callers
return True return True
state.mark(t) f.set_checked()
f.save()
return False return False
def should_build(t): def should_build(t):
return not state.isbuilt(t) and dirty_deps(t, depth = '') f = state.File(name=t)
return dirty_deps(f, depth = '', max_changed = vars.RUNID)
rv = 202 rv = 202
try: try:
me = os.path.join(vars.STARTDIR, me = os.path.join(vars.STARTDIR,
os.path.join(vars.PWD, vars.TARGET)) os.path.join(vars.PWD, vars.TARGET))
f = state.File(name=me)
debug2('TARGET: %r %r %r\n' % (vars.STARTDIR, vars.PWD, vars.TARGET)) debug2('TARGET: %r %r %r\n' % (vars.STARTDIR, vars.PWD, vars.TARGET))
try: try:
targets = sys.argv[1:] targets = sys.argv[1:]
for t in targets: for t in targets:
state.add_dep(me, 'm', t) f.add_dep('m', t)
f.save()
rv = builder.main(targets, should_build) rv = builder.main(targets, should_build)
finally: finally:
jwack.force_return_tokens() jwack.force_return_tokens()

View file

@ -1,15 +1,16 @@
#!/usr/bin/python #!/usr/bin/python
import sys, os import sys, os
import vars, state import vars, state
from helpers import err, mkdirp from helpers import err
try: try:
me = state.File(name=vars.TARGET)
for t in sys.argv[1:]: for t in sys.argv[1:]:
if os.path.exists(t): if os.path.exists(t):
err('redo-ifcreate: error: %r already exists\n' % t) err('redo-ifcreate: error: %r already exists\n' % t)
sys.exit(1) sys.exit(1)
else: else:
state.add_dep(vars.TARGET, 'c', t) me.add_dep('c', t)
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit(200) sys.exit(200)

396
state.py
View file

@ -1,29 +1,114 @@
import sys, os, errno, glob import sys, os, errno, glob, stat, fcntl, sqlite3
import vars import vars
from helpers import unlink, err, debug2, debug3, mkdirp, close_on_exec from helpers import unlink, err, debug2, debug3, close_on_exec
import helpers
SCHEMA_VER=1
TIMEOUT=60
def _connect(dbfile):
_db = sqlite3.connect(dbfile, timeout=TIMEOUT)
_db.execute("pragma synchronous = off")
_db.execute("pragma journal_mode = PERSIST")
return _db
_db = None
_lockfile = None
def db():
global _db, _lockfile
if _db:
return _db
dbdir = '%s/.redo' % vars.BASE
dbfile = '%s/db.sqlite3' % dbdir
try:
os.mkdir(dbdir)
except OSError, e:
if e.errno == errno.EEXIST:
pass # if it exists, that's okay
else:
raise
_lockfile = os.open(os.path.join(vars.BASE, '.redo/locks'),
os.O_RDWR | os.O_CREAT, 0666)
close_on_exec(_lockfile, True)
must_create = not os.path.exists(dbfile)
if not must_create:
_db = _connect(dbfile)
try:
row = _db.cursor().execute("select version from Schema").fetchone()
except sqlite3.OperationalError:
row = None
ver = row and row[0] or None
if ver != SCHEMA_VER:
err("state database: discarding v%s (wanted v%s)\n"
% (ver, SCHEMA_VER))
must_create = True
_db = None
if must_create:
unlink(dbfile)
_db = _connect(dbfile)
_db.execute("create table Schema "
" (version int)")
_db.execute("create table Runid "
" (id integer primary key autoincrement)")
_db.execute("create table Files "
" (name not null primary key, "
" is_generated int, "
" checked_runid int, "
" changed_runid int, "
" stamp, "
" csum)")
_db.execute("create table Deps "
" (target int, "
" source int, "
" mode not null, "
" primary key (target,source))")
_db.execute("insert into Schema (version) values (?)", [SCHEMA_VER])
# eat the '0' runid and File id
_db.execute("insert into Runid default values")
_db.execute("insert into Files (name) values (?)", [''])
if not vars.RUNID:
_db.execute("insert into Runid default values")
vars.RUNID = _db.execute("select last_insert_rowid()").fetchone()[0]
os.environ['REDO_RUNID'] = str(vars.RUNID)
_db.commit()
return _db
def init(): def init():
# FIXME: just wiping out all the locks is kind of cheating. But we db()
# 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. _wrote = 0
mkdirp('%s/.redo' % vars.BASE) def _write(q, l):
for f in glob.glob('%s/.redo/lock*' % vars.BASE): if _insane:
os.unlink(f) return
for f in glob.glob('%s/.redo/mark^*' % vars.BASE): global _wrote
os.unlink(f) _wrote += 1
for f in glob.glob('%s/.redo/built^*' % vars.BASE): #helpers.log_('W: %r %r\n' % (q,l))
os.unlink(f) db().execute(q, l)
def commit():
if _insane:
return
global _wrote
if _wrote:
#helpers.log_("COMMIT (%d)\n" % _wrote)
db().commit()
_wrote = 0
_insane = None _insane = None
def is_sane(): def check_sane():
global _insane global _insane, _writable
if not _insane: if not _insane:
_insane = not os.path.exists('%s/.redo' % vars.BASE) _insane = not os.path.exists('%s/.redo' % vars.BASE)
if _insane:
err('.redo directory disappeared; cannot continue.\n')
return not _insane return not _insane
@ -46,185 +131,154 @@ def relpath(t, base):
return '/'.join(tparts) return '/'.join(tparts)
def _sname(typ, t): class File(object):
# FIXME: t.replace(...) is non-reversible and non-unique here! # use this mostly to avoid accidentally assigning to typos
tnew = relpath(t, vars.BASE) __slots__ = ['id', 'name', 'is_generated',
v = vars.BASE + ('/.redo/%s^%s' % (typ, tnew.replace('/', '^'))) 'checked_runid', 'changed_runid',
if vars.DEBUG >= 3: 'stamp', 'csum']
debug3('sname: (%r) %r -> %r\n' % (os.getcwd(), t, tnew))
return v
def _init_from_cols(self, cols):
(self.id, self.name, self.is_generated,
self.checked_runid, self.changed_runid,
self.stamp, self.csum) = cols
def add_dep(t, mode, dep): def __init__(self, id=None, name=None, cols=None):
sn = _sname('dep', t) if cols:
return self._init_from_cols(cols)
q = ('select rowid, name, is_generated, checked_runid, changed_runid, '
' stamp, csum '
' from Files ')
if id != None:
q += 'where rowid=?'
l = [id]
elif name != None:
name = relpath(name, vars.BASE)
q += 'where name=?'
l = [name]
else:
raise Exception('name or id must be set')
d = db()
row = d.execute(q, l).fetchone()
if not row:
if not name:
raise Exception('File with id=%r not found and '
'name not given' % id)
try:
_write('insert into Files (name) values (?)', [name])
except sqlite3.IntegrityError:
# some parallel redo probably added it at the same time; no
# big deal.
pass
row = d.execute(q, l).fetchone()
assert(row)
self._init_from_cols(row)
def save(self):
_write('update Files set '
' is_generated=?, checked_runid=?, changed_runid=?, '
' stamp=?, csum=? '
' where rowid=?',
[self.is_generated,
self.checked_runid, self.changed_runid,
self.stamp, self.csum,
self.id])
def set_checked(self):
self.checked_runid = vars.RUNID
def set_changed(self):
debug2('BUILT: %r (%r)\n' % (self.name, self.stamp))
self.changed_runid = vars.RUNID
def set_static(self):
self.update_stamp()
def update_stamp(self):
newstamp = self.read_stamp()
if newstamp != self.stamp:
debug2("STAMP: %s: %r -> %r\n" % (self.name, self.stamp, newstamp))
self.stamp = newstamp
self.set_changed()
def is_changed(self):
return self.changed_runid and self.changed_runid >= vars.RUNID
def is_checked(self):
return self.checked_runid and self.checked_runid >= vars.RUNID
def deps(self):
q = ('select Deps.mode, Deps.source, '
' name, is_generated, checked_runid, changed_runid, '
' stamp, csum '
' from Files '
' join Deps on Files.rowid = Deps.source '
' where target=?')
for row in db().execute(q, [self.id]).fetchall():
mode = row[0]
cols = row[1:]
assert(mode in ('c', 'm'))
yield mode,File(cols=cols)
def zap_deps(self):
debug2('zap-deps: %r\n' % self.name)
_write('delete from Deps where target=?', [self.id])
def add_dep(self, mode, dep):
src = File(name=dep)
reldep = relpath(dep, vars.BASE) reldep = relpath(dep, vars.BASE)
debug2('add-dep: %r < %s %r\n' % (sn, mode, reldep)) debug2('add-dep: %r < %s %r\n' % (self.name, mode, reldep))
assert(src.name == reldep)
_write("insert or replace into Deps "
" (target, mode, source) values (?,?,?)",
[self.id, mode, src.id])
open(sn, 'a').write('%s %s\n' % (mode, reldep)) def read_stamp(self):
def deps(t):
for line in open(_sname('dep', t)).readlines():
assert(line[0] in ('c','m'))
assert(line[1] == ' ')
assert(line[-1] == '\n')
mode = line[0]
name = line[2:-1]
yield mode,name
def _stampname(t):
return _sname('stamp', t)
def stamp(t):
mark(t)
stampfile = _stampname(t)
newstampfile = _sname('stamp' + str(os.getpid()), t)
depfile = _sname('dep', t)
if not os.path.exists(vars.BASE + '/.redo'):
# .redo might not exist in a 'make clean' target
return
open(newstampfile, 'w').close()
try: try:
mtime = os.stat(t).st_mtime st = os.stat(os.path.join(vars.BASE, self.name))
except OSError: except OSError:
mtime = 0 return '0' # does not exist
os.utime(newstampfile, (mtime, mtime)) if stat.S_ISDIR(st.st_mode):
os.rename(newstampfile, stampfile) return 'dir' # the timestamp of a directory is meaningless
open(depfile, 'a').close()
def unstamp(t):
unlink(_stampname(t))
unlink(_sname('dep', t))
def unmark_as_generated(t):
unstamp(t)
unlink(_sname('gen', t))
def stamped(t):
try:
stamptime = os.stat(_stampname(t)).st_mtime
except OSError, e:
if e.errno == errno.ENOENT:
return None
else: else:
raise # a "unique identifier" stamp for a regular file
return stamptime return str((st.st_ctime, st.st_mtime, st.st_size, st.st_ino))
def built(t):
try:
open(_sname('built', t), 'w').close()
except IOError, e:
if e.errno == errno.ENOENT:
pass # may happen if someone deletes our .redo dir
else:
raise
_builts = {}
def isbuilt(t):
if _builts.get(t):
return True
if os.path.exists(_sname('built', t)):
_builts[t] = True
return True
# stamps the given input file, but only considers it to have been "built" if its
# mtime has changed. This is useful for static (non-generated) files.
def stamp_and_maybe_built(t):
if stamped(t) != os.stat(t).st_mtime:
built(t)
stamp(t)
def mark(t):
try:
open(_sname('mark', t), 'w').close()
except IOError, e:
if e.errno == errno.ENOENT:
pass # may happen if someone deletes our .redo dir
else:
raise
_marks = {}
def ismarked(t):
if _marks.get(t):
return True
if os.path.exists(_sname('mark', t)):
_marks[t] = True
return True
def is_generated(t):
return os.path.exists(_sname('gen', t))
def start(t):
unstamp(t)
open(_sname('dep', t), 'w').close()
open(_sname('gen', t), 'w').close() # it's definitely a generated file
# FIXME: I really want to use fcntl F_SETLK, F_SETLKW, etc here. But python
# doesn't do the lockdata structure in a portable way, so we have to use
# fcntl.lockf() instead. Usually this is just a wrapper for fcntl, so it's
# ok, but it doesn't have F_GETLK, so we can't report which pid owns the lock.
# The makes debugging a bit harder. When we someday port to C, we can do that.
class Lock: class Lock:
def __init__(self, t): def __init__(self, fid):
assert(_lockfile >= 0)
self.owned = False self.owned = False
self.rfd = self.wfd = None self.fid = fid
self.lockname = _sname('lock', t)
def __del__(self): def __del__(self):
if self.owned: if self.owned:
self.unlock() self.unlock()
def trylock(self): def trylock(self):
assert(not self.owned)
try: try:
os.mkfifo(self.lockname, 0600) fcntl.lockf(_lockfile, fcntl.LOCK_EX|fcntl.LOCK_NB, 1, self.fid)
self.owned = True except IOError, e:
self.rfd = os.open(self.lockname, os.O_RDONLY|os.O_NONBLOCK) if e.errno in (errno.EAGAIN, errno.EACCES):
self.wfd = os.open(self.lockname, os.O_WRONLY) pass # someone else has it locked
close_on_exec(self.rfd, True)
close_on_exec(self.wfd, True)
except OSError, e:
if e.errno == errno.EEXIST:
pass
else: else:
raise raise
else:
self.owned = True
def waitlock(self): def waitlock(self):
while not self.owned: assert(not self.owned)
self.wait() fcntl.lockf(_lockfile, fcntl.LOCK_EX, 1, self.fid)
self.trylock() self.owned = True
assert(self.owned)
def unlock(self): def unlock(self):
if not self.owned: if not self.owned:
raise Exception("can't unlock %r - we don't own it" raise Exception("can't unlock %r - we don't own it"
% self.lockname) % self.lockname)
unlink(self.lockname) fcntl.lockf(_lockfile, fcntl.LOCK_UN, 1, self.fid)
# ping any connected readers
os.close(self.rfd)
os.close(self.wfd)
self.rfd = self.wfd = None
self.owned = False self.owned = False
def wait(self):
if self.owned:
raise Exception("can't wait on %r - we own it" % self.lockname)
try:
# open() will finish only when a writer exists and does close()
fd = os.open(self.lockname, os.O_RDONLY)
try:
os.read(fd, 1)
finally:
os.close(fd)
except OSError, e:
if e.errno == errno.ENOENT:
pass # it's not even unlocked or was unlocked earlier
else:
raise

View file

@ -2,15 +2,15 @@ rm -f chdir1
redo chdir2 redo chdir2
redo chdir3 redo chdir3
. ./flush-cache.sh ./flush-cache.sh
redo-ifchange chdir3 redo-ifchange chdir3
rm -f chdir1 rm -f chdir1
. ./flush-cache.sh ./flush-cache.sh
redo-ifchange chdir3 redo-ifchange chdir3
[ -e chdir1 ] || exit 77 [ -e chdir1 ] || exit 77
rm -f chdir1 rm -f chdir1
. ./flush-cache.sh ./flush-cache.sh
redo-ifchange chdir3 redo-ifchange chdir3
[ -e chdir1 ] || exit 78 [ -e chdir1 ] || exit 78

View file

@ -1,10 +1,10 @@
rm -f *.out *.log rm -f *.out *.log
. ../../flush-cache.sh ../../flush-cache.sh
redo-ifchange 1.out 2.out redo-ifchange 1.out 2.out
[ "$(cat 1.log | wc -l)" = 1 ] || exit 55 [ "$(cat 1.log | wc -l)" = 1 ] || exit 55
[ "$(cat 2.log | wc -l)" = 1 ] || exit 56 [ "$(cat 2.log | wc -l)" = 1 ] || exit 56
. ../../flush-cache.sh ../../flush-cache.sh
touch 1.in touch 1.in
redo-ifchange 1.out 2.out redo-ifchange 1.out 2.out
[ "$(cat 1.log | wc -l)" = 2 ] || exit 57 [ "$(cat 1.log | wc -l)" = 2 ] || exit 57

View file

@ -1,11 +1,11 @@
rm -f log dir1/log dir1/stinky rm -f log dir1/log dir1/stinky
touch t1.do touch t1.do
. ../../flush-cache.sh ../../flush-cache.sh
redo t1 redo t1
touch t1.do touch t1.do
. ../../flush-cache.sh ../../flush-cache.sh
redo t1 redo t1
. ../../flush-cache.sh ../../flush-cache.sh
redo-ifchange t1 redo-ifchange t1
C1="$(wc -l <dir1/log)" C1="$(wc -l <dir1/log)"
C2="$(wc -l <log)" C2="$(wc -l <log)"

View file

@ -3,7 +3,7 @@ rm -f static.log
redo static1 static2 redo static1 static2
touch static.in touch static.in
. ../flush-cache.sh ../flush-cache.sh
redo-ifchange static1 static2 redo-ifchange static1 static2
COUNT=$(wc -l <static.log) COUNT=$(wc -l <static.log)

View file

@ -1,19 +1,19 @@
rm -f genfile2 genfile2.do genfile.log rm -f genfile2 genfile2.do genfile.log
echo echo hello >genfile2.do echo echo hello >genfile2.do
. ../flush-cache.sh ../flush-cache.sh
redo genfile1 redo genfile1
# this will cause a rebuild: # this will cause a rebuild:
# genfile1 depends on genfile2 depends on genfile2.do # genfile1 depends on genfile2 depends on genfile2.do
rm -f genfile2.do rm -f genfile2.do
. ../flush-cache.sh ../flush-cache.sh
redo-ifchange genfile1 redo-ifchange genfile1
# but genfile2.do was gone last time, so genfile2 no longer depends on it. # but genfile2.do was gone last time, so genfile2 no longer depends on it.
# thus, it can be considered up-to-date. Prior versions of redo had a bug # thus, it can be considered up-to-date. Prior versions of redo had a bug
# where the dependency on genfile2.do was never dropped. # where the dependency on genfile2.do was never dropped.
. ../flush-cache.sh ../flush-cache.sh
redo-ifchange genfile1 redo-ifchange genfile1
COUNT=$(wc -l <genfile.log) COUNT=$(wc -l <genfile.log)

View file

@ -6,6 +6,7 @@ if [ -e t1a ]; then
else else
BEFORE= BEFORE=
fi fi
../flush-cache.sh
redo-ifchange t1a # it definitely had to rebuild because t1dep changed redo-ifchange t1a # it definitely had to rebuild because t1dep changed
AFTER="$(cat t1a)" AFTER="$(cat t1a)"
if [ "$BEFORE" = "$AFTER" ]; then if [ "$BEFORE" = "$AFTER" ]; then

9
t/flush-cache.sh Normal file → Executable file
View file

@ -1,3 +1,8 @@
#!/bin/sh
#echo "Flushing redo cache..." >&2 #echo "Flushing redo cache..." >&2
find "$REDO_BASE/.redo" -name 'built^*' -o -name 'mark^*' | (
xargs rm -f >&2 echo ".timeout 5000"
echo "pragma synchronous = off;"
echo "update Files set checked_runid=null, " \
" changed_runid=changed_runid-1;"
) | sqlite3 "$REDO_BASE/.redo/db.sqlite3"

View file

@ -1,7 +1,7 @@
rm -f makedir.log rm -f makedir.log
redo makedir redo makedir
touch makedir/outfile touch makedir/outfile
. ./flush-cache.sh ./flush-cache.sh
redo-ifchange makedir redo-ifchange makedir
COUNT=$(wc -l <makedir.log) COUNT=$(wc -l <makedir.log)
[ "$COUNT" = 1 ] || exit 99 [ "$COUNT" = 1 ] || exit 99

View file

@ -18,6 +18,7 @@ XTRACE = os.environ.get('REDO_XTRACE', '') and 1 or 0
KEEP_GOING = os.environ.get('REDO_KEEP_GOING', '') and 1 or 0 KEEP_GOING = os.environ.get('REDO_KEEP_GOING', '') and 1 or 0
SHUFFLE = os.environ.get('REDO_SHUFFLE', '') and 1 or 0 SHUFFLE = os.environ.get('REDO_SHUFFLE', '') and 1 or 0
STARTDIR = os.environ['REDO_STARTDIR'] STARTDIR = os.environ['REDO_STARTDIR']
RUNID = atoi.atoi(os.environ.get('REDO_RUNID')) or None
BASE = os.environ['REDO_BASE'] BASE = os.environ['REDO_BASE']
while BASE and BASE.endswith('/'): while BASE and BASE.endswith('/'):
BASE = BASE[:-1] BASE = BASE[:-1]