216 lines
7.6 KiB
Python
216 lines
7.6 KiB
Python
# Simple abstract dependency system suitable for generating things like
|
|
# makefiles.
|
|
|
|
import util
|
|
|
|
import os
|
|
|
|
class DestFile:
|
|
def __init__(self, name, simplename, dep_files={}, parameterization={}):
|
|
# TODO: Separate "my" parameterization from "dep" parameterization
|
|
self.name = name
|
|
if simplename:
|
|
self.simplename = simplename
|
|
else:
|
|
self.simplename = name
|
|
self.parameterization = parameterization
|
|
self.dep_files = dep_files
|
|
def __repr__(self):
|
|
return self.name
|
|
def __hash__(self):
|
|
return hash(self.name) ^ hash(self.simplename)
|
|
def __cmp__(self, other):
|
|
if isinstance(other, DestFile):
|
|
return cmp(self.name, other.name)
|
|
else:
|
|
return NotImplemented
|
|
|
|
class DepState:
|
|
def __init__(self, files, params):
|
|
self.files = files
|
|
self.params = params
|
|
self.parameterization = {}
|
|
for file in filemap_to_files(files):
|
|
self.parameterization.update(file.parameterization)
|
|
|
|
class DepSys:
|
|
def __init__(self):
|
|
self.runs = {}
|
|
def begin(self, state):
|
|
raise Exception("DepSys not overridden")
|
|
|
|
class DepSysEntry:
|
|
"""Abstract dependency system entry.
|
|
|
|
You must override this and provide a way of generating filenames.
|
|
"""
|
|
def __init__(self, sys, state):
|
|
"""
|
|
sys - a DepSys object
|
|
state - a DepState object
|
|
"""
|
|
self.sys = sys
|
|
self.state = state
|
|
def file(self, simplename, *extra_parameterization):
|
|
parameterization = dict(self.state.parameterization)
|
|
my_parameterization = dict([(k, self.state.params[k])
|
|
for k in extra_parameterization])
|
|
parameterization.update(my_parameterization)
|
|
realname = self._make_name(simplename, parameterization)
|
|
self.ensure_writable(realname)
|
|
return DestFile(realname, simplename, self.state.files, parameterization)
|
|
def _make_name(self, simplename, parameterization):
|
|
raise Exception("not implemented")
|
|
def ensure_writable(realname):
|
|
raise Exception("not implemented")
|
|
def end(self, files):
|
|
raise Exception("not implemented")
|
|
def command(self, str):
|
|
raise Exception("not implemented")
|
|
|
|
class BadResult:
|
|
pass
|
|
|
|
class FileCollection:
|
|
def __init__(self, pairs):
|
|
self.files_by_type = {}
|
|
# use seen_pairs to avoid adding same file twice
|
|
seen_pairs = {}
|
|
for (typename, file) in pairs:
|
|
if not (typename, file) in seen_pairs:
|
|
seen_pairs[(typename, file)] = 1
|
|
self.files_by_type.setdefault(typename, []).append(file)
|
|
def single(self, typename):
|
|
result = self.files_by_type[typename]
|
|
if len(result) != 1:
|
|
raise BadResult()
|
|
return result[0]
|
|
def many(self, typename):
|
|
return self.files_by_type.get(typename, [])
|
|
def to_pairs(self):
|
|
pairs = []
|
|
for (classname, files) in self.files_by_type.items():
|
|
pairs += [(classname, file) for file in files]
|
|
return pairs
|
|
def to_files(self):
|
|
return util.collapse_once(
|
|
[files for (classname, files) in self.files_by_type.items()])
|
|
def to_names(self):
|
|
return [f.name for f in self.to_files()]
|
|
|
|
def filemap_to_files(filemap):
|
|
return util.collapse_once(
|
|
[file_collection.to_files() for file_collection in filemap.values()])
|
|
|
|
class Metarule:
|
|
"""
|
|
Generic rule.
|
|
|
|
This rule takes in several labelled sets of other rules.
|
|
|
|
TODO: Explain the idea of "meta-rule" better
|
|
|
|
This is more a meta-rule, which can depend on other meta-rules.
|
|
Meta-rules are asked for the exact filenames they produce, which
|
|
recursively ask their dependency rules what files they produce. Once a
|
|
meta-rule knows what files its dependency rules produce, it can generate
|
|
commands to produce its own result files, and tell ITS dependents what
|
|
files it generated.
|
|
|
|
"""
|
|
|
|
# TODO: (requires thought) If multiple rules depend on a single rule, I
|
|
# *think* this code, how it is written it will run "doit" for that rule
|
|
# multiple times, rather than once. This might be necessary due to
|
|
# parameterization, I haven't thought much about it.
|
|
|
|
def __init__(self, **dep_map):
|
|
self.dep_map = dep_map
|
|
def generate(self, sys, params):
|
|
def fake_cont(x):
|
|
pass
|
|
self.run(sys, params, fake_cont)
|
|
def run(self, sys, params, continuation):
|
|
key = (self, util.dicthash(params))
|
|
if key in sys.runs:
|
|
continuation(sys.runs[key])
|
|
else:
|
|
all_deps = []
|
|
|
|
for (dep_class, deps) in self.dep_map.items():
|
|
for dep in deps:
|
|
all_deps.append((dep_class, dep))
|
|
|
|
dep_count = [len(all_deps)]
|
|
dep_files = {}
|
|
|
|
def check_done():
|
|
if dep_count[0] == 0:
|
|
dep_files_converted = dict(self.dep_map)
|
|
for k in self.dep_map:
|
|
dep_files_converted[k] = FileCollection(dep_files.get(k, []))
|
|
state = DepState(dep_files_converted, params)
|
|
# Also memoize here!
|
|
action = sys.begin(state)
|
|
my_files = self.doit(action, dep_files_converted, params)
|
|
action.end(my_files)
|
|
sys.runs[key] = my_files
|
|
continuation(my_files)
|
|
dep_count[0] = -1
|
|
|
|
def my_continuation_for(dep_class):
|
|
def impl(single_dep_files):
|
|
dep_files.setdefault(dep_class, []).extend(single_dep_files)
|
|
dep_count[0] -= 1
|
|
check_done()
|
|
return impl
|
|
|
|
for (dep_class, dep) in all_deps:
|
|
dep.run(sys, params, my_continuation_for(dep_class))
|
|
|
|
check_done()
|
|
def doit(self, realrule, dep_files, params):
|
|
"""
|
|
The doit function is called for all subclasses of this. The doit
|
|
function doesn't necessarily perform the action, but is required to
|
|
figure out exactly what actions are required. For instance, for the
|
|
build system, this generates a Makefile rule, which contains the exact
|
|
commands and file names.
|
|
|
|
The following are sent to an implementors' doit:
|
|
- dep_files: the files "generated" by all rules it depends on. The rules
|
|
are grouped into the label of the rule (given when you called
|
|
Metarule.__init__) and then into the type of the file (like file
|
|
extension). The rules you are dependent on were the ones responsible
|
|
for assigning the file extension. Each file has a "simple" name and
|
|
an "absolute" name. The simple name should be used for generating
|
|
result files, and the absolute name is the location of the source
|
|
file.
|
|
- params: the parameterization of the instantiation; that is, each
|
|
file can be created with combinations of different parameters, and the
|
|
parameterization is used to identify different "builds" of the same
|
|
file. in the case of the build system, this is different operating
|
|
systems or compilation modes. certain rules care about certain
|
|
parameters, so when they generate destination files, they specify
|
|
which parameters they care about.
|
|
- the state (an encapsulation of params and input files)
|
|
TODO: Remember why this is here, it doesn't seem to be used anywhere
|
|
- systentry: a single entry in thie build system. This is equivalent
|
|
to a Makefile rule waiting for you to put actions into it.
|
|
Unlike this Metarule class, this object wants exact specific commands
|
|
for a specific parameterization, like a Makefile rule with real
|
|
filenames and commands.
|
|
You must return:
|
|
- a list of key,val pairs, where key is some indicator of file type.
|
|
Example is [("header", "foo.h"), ("source", "foo.cc")].
|
|
"""
|
|
# TODO: Implement me in subclasses
|
|
return util.map_values(FileCollection.to_pairs, dep_files)
|
|
|
|
class CustomRule(Metarule):
|
|
def __init__(self, dep_map, doit_fn):
|
|
Metarule.__init__(self, **dep_map)
|
|
self.doit_fn = doit_fn
|
|
def doit(self, realrule, dep_files, params):
|
|
return self.doit_fn(realrule, dep_files, params)
|