| """ |
| A representation of makefile data structures. |
| """ |
| |
| import logging, re, os, sys |
| import parserdata, parser, functions, process, util, implicit |
| from cStringIO import StringIO |
| |
| if sys.version_info[0] < 3: |
| str_type = basestring |
| else: |
| str_type = str |
| |
| _log = logging.getLogger('pymake.data') |
| |
| class DataError(util.MakeError): |
| pass |
| |
| class ResolutionError(DataError): |
| """ |
| Raised when dependency resolution fails, either due to recursion or to missing |
| prerequisites.This is separately catchable so that implicit rule search can try things |
| without having to commit. |
| """ |
| pass |
| |
| def withoutdups(it): |
| r = set() |
| for i in it: |
| if not i in r: |
| r.add(i) |
| yield i |
| |
| def mtimeislater(deptime, targettime): |
| """ |
| Is the mtime of the dependency later than the target? |
| """ |
| |
| if deptime is None: |
| return True |
| if targettime is None: |
| return False |
| # int(1000*x) because of http://bugs.python.org/issue10148 |
| return int(1000 * deptime) > int(1000 * targettime) |
| |
| def getmtime(path): |
| try: |
| s = os.stat(path) |
| return s.st_mtime |
| except OSError: |
| return None |
| |
| def stripdotslash(s): |
| if s.startswith('./'): |
| st = s[2:] |
| return st if st != '' else '.' |
| return s |
| |
| def stripdotslashes(sl): |
| for s in sl: |
| yield stripdotslash(s) |
| |
| def getindent(stack): |
| return ''.ljust(len(stack) - 1) |
| |
| def _if_else(c, t, f): |
| if c: |
| return t() |
| return f() |
| |
| |
| class BaseExpansion(object): |
| """Base class for expansions. |
| |
| A make expansion is the parsed representation of a string, which may |
| contain references to other elements. |
| """ |
| |
| @property |
| def is_static_string(self): |
| """Returns whether the expansion is composed of static string content. |
| |
| This is always True for StringExpansion. It will be True for Expansion |
| only if all elements of that Expansion are static strings. |
| """ |
| raise Exception('Must be implemented in child class.') |
| |
| def functions(self, descend=False): |
| """Obtain all functions inside this expansion. |
| |
| This is a generator for pymake.functions.Function instances. |
| |
| By default, this only returns functions existing as the primary |
| elements of this expansion. If `descend` is True, it will descend into |
| child expansions and extract all functions in the tree. |
| """ |
| # An empty generator. Yeah, it's weird. |
| for x in []: |
| yield x |
| |
| def variable_references(self, descend=False): |
| """Obtain all variable references in this expansion. |
| |
| This is a generator for pymake.functionsVariableRef instances. |
| |
| To retrieve the names of variables, simply query the `vname` field on |
| the returned instances. Most of the time these will be StringExpansion |
| instances. |
| """ |
| for f in self.functions(descend=descend): |
| if not isinstance(f, functions.VariableRef): |
| continue |
| |
| yield f |
| |
| @property |
| def is_filesystem_dependent(self): |
| """Whether this expansion may query the filesystem for evaluation. |
| |
| This effectively asks "is any function in this expansion dependent on |
| the filesystem. |
| """ |
| for f in self.functions(descend=True): |
| if f.is_filesystem_dependent: |
| return True |
| |
| return False |
| |
| @property |
| def is_shell_dependent(self): |
| """Whether this expansion may invoke a shell for evaluation.""" |
| |
| for f in self.functions(descend=True): |
| if isinstance(f, functions.ShellFunction): |
| return True |
| |
| return False |
| |
| |
| class StringExpansion(BaseExpansion): |
| """An Expansion representing a static string. |
| |
| This essentially wraps a single str instance. |
| """ |
| |
| __slots__ = ('loc', 's',) |
| simple = True |
| |
| def __init__(self, s, loc): |
| assert isinstance(s, str_type) |
| self.s = s |
| self.loc = loc |
| |
| def lstrip(self): |
| self.s = self.s.lstrip() |
| |
| def rstrip(self): |
| self.s = self.s.rstrip() |
| |
| def isempty(self): |
| return self.s == '' |
| |
| def resolve(self, i, j, fd, k=None): |
| fd.write(self.s) |
| |
| def resolvestr(self, i, j, k=None): |
| return self.s |
| |
| def resolvesplit(self, i, j, k=None): |
| return self.s.split() |
| |
| def clone(self): |
| e = Expansion(self.loc) |
| e.appendstr(self.s) |
| return e |
| |
| @property |
| def is_static_string(self): |
| return True |
| |
| def __len__(self): |
| return 1 |
| |
| def __getitem__(self, i): |
| assert i == 0 |
| return self.s, False |
| |
| def __repr__(self): |
| return "Exp<%s>(%r)" % (self.loc, self.s) |
| |
| def __eq__(self, other): |
| """We only compare the string contents.""" |
| return self.s == other |
| |
| def __ne__(self, other): |
| return not self.__eq__(other) |
| |
| def to_source(self, escape_variables=False, escape_comments=False): |
| s = self.s |
| |
| if escape_comments: |
| s = s.replace('#', '\\#') |
| |
| if escape_variables: |
| return s.replace('$', '$$') |
| |
| return s |
| |
| |
| class Expansion(BaseExpansion, list): |
| """A representation of expanded data. |
| |
| This is effectively an ordered list of StringExpansion and |
| pymake.function.Function instances. Every item in the collection appears in |
| the same context in a make file. |
| """ |
| |
| __slots__ = ('loc',) |
| simple = False |
| |
| def __init__(self, loc=None): |
| # A list of (element, isfunc) tuples |
| # element is either a string or a function |
| self.loc = loc |
| |
| @staticmethod |
| def fromstring(s, path): |
| return StringExpansion(s, parserdata.Location(path, 1, 0)) |
| |
| def clone(self): |
| e = Expansion() |
| e.extend(self) |
| return e |
| |
| def appendstr(self, s): |
| assert isinstance(s, str_type) |
| if s == '': |
| return |
| |
| self.append((s, False)) |
| |
| def appendfunc(self, func): |
| assert isinstance(func, functions.Function) |
| self.append((func, True)) |
| |
| def concat(self, o): |
| """Concatenate the other expansion on to this one.""" |
| if o.simple: |
| self.appendstr(o.s) |
| else: |
| self.extend(o) |
| |
| def isempty(self): |
| return (not len(self)) or self[0] == ('', False) |
| |
| def lstrip(self): |
| """Strip leading literal whitespace from this expansion.""" |
| while True: |
| i, isfunc = self[0] |
| if isfunc: |
| return |
| |
| i = i.lstrip() |
| if i != '': |
| self[0] = i, False |
| return |
| |
| del self[0] |
| |
| def rstrip(self): |
| """Strip trailing literal whitespace from this expansion.""" |
| while True: |
| i, isfunc = self[-1] |
| if isfunc: |
| return |
| |
| i = i.rstrip() |
| if i != '': |
| self[-1] = i, False |
| return |
| |
| del self[-1] |
| |
| def finish(self): |
| # Merge any adjacent literal strings: |
| strings = [] |
| elements = [] |
| for (e, isfunc) in self: |
| if isfunc: |
| if strings: |
| s = ''.join(strings) |
| if s: |
| elements.append((s, False)) |
| strings = [] |
| elements.append((e, True)) |
| else: |
| strings.append(e) |
| |
| if not elements: |
| # This can only happen if there were no function elements. |
| return StringExpansion(''.join(strings), self.loc) |
| |
| if strings: |
| s = ''.join(strings) |
| if s: |
| elements.append((s, False)) |
| |
| if len(elements) < len(self): |
| self[:] = elements |
| |
| return self |
| |
| def resolve(self, makefile, variables, fd, setting=[]): |
| """ |
| Resolve this variable into a value, by interpolating the value |
| of other variables. |
| |
| @param setting (Variable instance) the variable currently |
| being set, if any. Setting variables must avoid self-referential |
| loops. |
| """ |
| assert isinstance(makefile, Makefile) |
| assert isinstance(variables, Variables) |
| assert isinstance(setting, list) |
| |
| for e, isfunc in self: |
| if isfunc: |
| e.resolve(makefile, variables, fd, setting) |
| else: |
| assert isinstance(e, str_type) |
| fd.write(e) |
| |
| def resolvestr(self, makefile, variables, setting=[]): |
| fd = StringIO() |
| self.resolve(makefile, variables, fd, setting) |
| return fd.getvalue() |
| |
| def resolvesplit(self, makefile, variables, setting=[]): |
| return self.resolvestr(makefile, variables, setting).split() |
| |
| @property |
| def is_static_string(self): |
| """An Expansion is static if all its components are strings, not |
| functions.""" |
| for e, is_func in self: |
| if is_func: |
| return False |
| |
| return True |
| |
| def functions(self, descend=False): |
| for e, is_func in self: |
| if is_func: |
| yield e |
| |
| if descend: |
| for exp in e.expansions(descend=True): |
| for f in exp.functions(descend=True): |
| yield f |
| |
| def __repr__(self): |
| return "<Expansion with elements: %r>" % ([e for e, isfunc in self],) |
| |
| def to_source(self, escape_variables=False, escape_comments=False): |
| parts = [] |
| for e, is_func in self: |
| if is_func: |
| parts.append(e.to_source()) |
| continue |
| |
| if escape_variables: |
| parts.append(e.replace('$', '$$')) |
| continue |
| |
| parts.append(e) |
| |
| return ''.join(parts) |
| |
| def __eq__(self, other): |
| if not isinstance(other, (Expansion, StringExpansion)): |
| return False |
| |
| # Expansions are equivalent if adjacent string literals normalize to |
| # the same value. So, we must normalize before any comparisons are |
| # made. |
| a = self.clone().finish() |
| |
| if isinstance(other, StringExpansion): |
| if isinstance(a, StringExpansion): |
| return a == other |
| |
| # A normalized Expansion != StringExpansion. |
| return False |
| |
| b = other.clone().finish() |
| |
| # b could be a StringExpansion now. |
| if isinstance(b, StringExpansion): |
| if isinstance(a, StringExpansion): |
| return a == b |
| |
| # Our normalized Expansion != normalized StringExpansion. |
| return False |
| |
| if len(a) != len(b): |
| return False |
| |
| for i in xrange(len(self)): |
| e1, is_func1 = a[i] |
| e2, is_func2 = b[i] |
| |
| if is_func1 != is_func2: |
| return False |
| |
| if type(e1) != type(e2): |
| return False |
| |
| if e1 != e2: |
| return False |
| |
| return True |
| |
| def __ne__(self, other): |
| return not self.__eq__(other) |
| |
| class Variables(object): |
| """ |
| A mapping from variable names to variables. Variables have flavor, source, and value. The value is an |
| expansion object. |
| """ |
| |
| __slots__ = ('parent', '_map') |
| |
| FLAVOR_RECURSIVE = 0 |
| FLAVOR_SIMPLE = 1 |
| FLAVOR_APPEND = 2 |
| |
| SOURCE_OVERRIDE = 0 |
| SOURCE_COMMANDLINE = 1 |
| SOURCE_MAKEFILE = 2 |
| SOURCE_ENVIRONMENT = 3 |
| SOURCE_AUTOMATIC = 4 |
| SOURCE_IMPLICIT = 5 |
| |
| def __init__(self, parent=None): |
| self._map = {} # vname -> flavor, source, valuestr, valueexp |
| self.parent = parent |
| |
| def readfromenvironment(self, env): |
| for k, v in env.iteritems(): |
| self.set(k, self.FLAVOR_RECURSIVE, self.SOURCE_ENVIRONMENT, v) |
| |
| def get(self, name, expand=True): |
| """ |
| Get the value of a named variable. Returns a tuple (flavor, source, value) |
| |
| If the variable is not present, returns (None, None, None) |
| |
| @param expand If true, the value will be returned as an expansion. If false, |
| it will be returned as an unexpanded string. |
| """ |
| flavor, source, valuestr, valueexp = self._map.get(name, (None, None, None, None)) |
| if flavor is not None: |
| if expand and flavor != self.FLAVOR_SIMPLE and valueexp is None: |
| d = parser.Data.fromstring(valuestr, parserdata.Location("Expansion of variables '%s'" % (name,), 1, 0)) |
| valueexp, t, o = parser.parsemakesyntax(d, 0, (), parser.iterdata) |
| self._map[name] = flavor, source, valuestr, valueexp |
| |
| if flavor == self.FLAVOR_APPEND: |
| if self.parent: |
| pflavor, psource, pvalue = self.parent.get(name, expand) |
| else: |
| pflavor, psource, pvalue = None, None, None |
| |
| if pvalue is None: |
| flavor = self.FLAVOR_RECURSIVE |
| # fall through |
| else: |
| if source > psource: |
| # TODO: log a warning? |
| return pflavor, psource, pvalue |
| |
| if not expand: |
| return pflavor, psource, pvalue + ' ' + valuestr |
| |
| pvalue = pvalue.clone() |
| pvalue.appendstr(' ') |
| pvalue.concat(valueexp) |
| |
| return pflavor, psource, pvalue |
| |
| if not expand: |
| return flavor, source, valuestr |
| |
| if flavor == self.FLAVOR_RECURSIVE: |
| val = valueexp |
| else: |
| val = Expansion.fromstring(valuestr, "Expansion of variable '%s'" % (name,)) |
| |
| return flavor, source, val |
| |
| if self.parent is not None: |
| return self.parent.get(name, expand) |
| |
| return (None, None, None) |
| |
| def set(self, name, flavor, source, value, force=False): |
| assert flavor in (self.FLAVOR_RECURSIVE, self.FLAVOR_SIMPLE) |
| assert source in (self.SOURCE_OVERRIDE, self.SOURCE_COMMANDLINE, self.SOURCE_MAKEFILE, self.SOURCE_ENVIRONMENT, self.SOURCE_AUTOMATIC, self.SOURCE_IMPLICIT) |
| assert isinstance(value, str_type), "expected str, got %s" % type(value) |
| |
| prevflavor, prevsource, prevvalue = self.get(name) |
| if prevsource is not None and source > prevsource and not force: |
| # TODO: give a location for this warning |
| _log.info("not setting variable '%s', set by higher-priority source to value '%s'" % (name, prevvalue)) |
| return |
| |
| self._map[name] = flavor, source, value, None |
| |
| def append(self, name, source, value, variables, makefile): |
| assert source in (self.SOURCE_OVERRIDE, self.SOURCE_MAKEFILE, self.SOURCE_AUTOMATIC) |
| assert isinstance(value, str_type) |
| |
| if name not in self._map: |
| self._map[name] = self.FLAVOR_APPEND, source, value, None |
| return |
| |
| prevflavor, prevsource, prevvalue, valueexp = self._map[name] |
| if source > prevsource: |
| # TODO: log a warning? |
| return |
| |
| if prevflavor == self.FLAVOR_SIMPLE: |
| d = parser.Data.fromstring(value, parserdata.Location("Expansion of variables '%s'" % (name,), 1, 0)) |
| valueexp, t, o = parser.parsemakesyntax(d, 0, (), parser.iterdata) |
| |
| val = valueexp.resolvestr(makefile, variables, [name]) |
| self._map[name] = prevflavor, prevsource, prevvalue + ' ' + val, None |
| return |
| |
| newvalue = prevvalue + ' ' + value |
| self._map[name] = prevflavor, prevsource, newvalue, None |
| |
| def merge(self, other): |
| assert isinstance(other, Variables) |
| for k, flavor, source, value in other: |
| self.set(k, flavor, source, value) |
| |
| def __iter__(self): |
| for k, (flavor, source, value, valueexp) in self._map.iteritems(): |
| yield k, flavor, source, value |
| |
| def __contains__(self, item): |
| return item in self._map |
| |
| class Pattern(object): |
| """ |
| A pattern is a string, possibly with a % substitution character. From the GNU make manual: |
| |
| '%' characters in pattern rules can be quoted with precending backslashes ('\'). Backslashes that |
| would otherwise quote '%' charcters can be quoted with more backslashes. Backslashes that |
| quote '%' characters or other backslashes are removed from the pattern before it is compared t |
| file names or has a stem substituted into it. Backslashes that are not in danger of quoting '%' |
| characters go unmolested. For example, the pattern the\%weird\\%pattern\\ has `the%weird\' preceding |
| the operative '%' character, and 'pattern\\' following it. The final two backslashes are left alone |
| because they cannot affect any '%' character. |
| |
| This insane behavior probably doesn't matter, but we're compatible just for shits and giggles. |
| """ |
| |
| __slots__ = ('data') |
| |
| def __init__(self, s): |
| r = [] |
| i = 0 |
| slen = len(s) |
| while i < slen: |
| c = s[i] |
| if c == '\\': |
| nc = s[i + 1] |
| if nc == '%': |
| r.append('%') |
| i += 1 |
| elif nc == '\\': |
| r.append('\\') |
| i += 1 |
| else: |
| r.append(c) |
| elif c == '%': |
| self.data = (''.join(r), s[i+1:]) |
| return |
| else: |
| r.append(c) |
| i += 1 |
| |
| # This is different than (s,) because \% and \\ have been unescaped. Parsing patterns is |
| # context-sensitive! |
| self.data = (''.join(r),) |
| |
| def ismatchany(self): |
| return self.data == ('','') |
| |
| def ispattern(self): |
| return len(self.data) == 2 |
| |
| def __hash__(self): |
| return self.data.__hash__() |
| |
| def __eq__(self, o): |
| assert isinstance(o, Pattern) |
| return self.data == o.data |
| |
| def gettarget(self): |
| assert not self.ispattern() |
| return self.data[0] |
| |
| def hasslash(self): |
| return self.data[0].find('/') != -1 or self.data[1].find('/') != -1 |
| |
| def match(self, word): |
| """ |
| Match this search pattern against a word (string). |
| |
| @returns None if the word doesn't match, or the matching stem. |
| If this is a %-less pattern, the stem will always be '' |
| """ |
| d = self.data |
| if len(d) == 1: |
| if word == d[0]: |
| return word |
| return None |
| |
| d0, d1 = d |
| l1 = len(d0) |
| l2 = len(d1) |
| if len(word) >= l1 + l2 and word.startswith(d0) and word.endswith(d1): |
| if l2 == 0: |
| return word[l1:] |
| return word[l1:-l2] |
| |
| return None |
| |
| def resolve(self, dir, stem): |
| if self.ispattern(): |
| return dir + self.data[0] + stem + self.data[1] |
| |
| return self.data[0] |
| |
| def subst(self, replacement, word, mustmatch): |
| """ |
| Given a word, replace the current pattern with the replacement pattern, a la 'patsubst' |
| |
| @param mustmatch If true and this pattern doesn't match the word, throw a DataError. Otherwise |
| return word unchanged. |
| """ |
| assert isinstance(replacement, str_type) |
| |
| stem = self.match(word) |
| if stem is None: |
| if mustmatch: |
| raise DataError("target '%s' doesn't match pattern" % (word,)) |
| return word |
| |
| if not self.ispattern(): |
| # if we're not a pattern, the replacement is not parsed as a pattern either |
| return replacement |
| |
| return Pattern(replacement).resolve('', stem) |
| |
| def __repr__(self): |
| return "<Pattern with data %r>" % (self.data,) |
| |
| _backre = re.compile(r'[%\\]') |
| def __str__(self): |
| if not self.ispattern(): |
| return self._backre.sub(r'\\\1', self.data[0]) |
| |
| return self._backre.sub(r'\\\1', self.data[0]) + '%' + self.data[1] |
| |
| class RemakeTargetSerially(object): |
| __slots__ = ('target', 'makefile', 'indent', 'rlist') |
| |
| def __init__(self, target, makefile, indent, rlist): |
| self.target = target |
| self.makefile = makefile |
| self.indent = indent |
| self.rlist = rlist |
| self.commandscb(False) |
| |
| def resolvecb(self, error, didanything): |
| assert error in (True, False) |
| |
| if didanything: |
| self.target.didanything = True |
| |
| if error: |
| self.target.error = True |
| self.makefile.error = True |
| if not self.makefile.keepgoing: |
| self.target.notifydone(self.makefile) |
| return |
| else: |
| # don't run the commands! |
| del self.rlist[0] |
| self.commandscb(error=False) |
| else: |
| self.rlist.pop(0).runcommands(self.indent, self.commandscb) |
| |
| def commandscb(self, error): |
| assert error in (True, False) |
| |
| if error: |
| self.target.error = True |
| self.makefile.error = True |
| |
| if self.target.error and not self.makefile.keepgoing: |
| self.target.notifydone(self.makefile) |
| return |
| |
| if not len(self.rlist): |
| self.target.notifydone(self.makefile) |
| else: |
| self.rlist[0].resolvedeps(True, self.resolvecb) |
| |
| class RemakeTargetParallel(object): |
| __slots__ = ('target', 'makefile', 'indent', 'rlist', 'rulesremaining', 'currunning') |
| |
| def __init__(self, target, makefile, indent, rlist): |
| self.target = target |
| self.makefile = makefile |
| self.indent = indent |
| self.rlist = rlist |
| |
| self.rulesremaining = len(rlist) |
| self.currunning = False |
| |
| for r in rlist: |
| makefile.context.defer(self.doresolve, r) |
| |
| def doresolve(self, r): |
| if self.makefile.error and not self.makefile.keepgoing: |
| r.error = True |
| self.resolvecb(True, False) |
| else: |
| r.resolvedeps(False, self.resolvecb) |
| |
| def resolvecb(self, error, didanything): |
| assert error in (True, False) |
| |
| if error: |
| self.target.error = True |
| |
| if didanything: |
| self.target.didanything = True |
| |
| self.rulesremaining -= 1 |
| |
| # commandscb takes care of the details if we're currently building |
| # something |
| if self.currunning: |
| return |
| |
| self.runnext() |
| |
| def runnext(self): |
| assert not self.currunning |
| |
| if self.makefile.error and not self.makefile.keepgoing: |
| self.rlist = [] |
| else: |
| while len(self.rlist) and self.rlist[0].error: |
| del self.rlist[0] |
| |
| if not len(self.rlist): |
| if not self.rulesremaining: |
| self.target.notifydone(self.makefile) |
| return |
| |
| if self.rlist[0].depsremaining != 0: |
| return |
| |
| self.currunning = True |
| rule = self.rlist.pop(0) |
| self.makefile.context.defer(rule.runcommands, self.indent, self.commandscb) |
| |
| def commandscb(self, error): |
| assert error in (True, False) |
| if error: |
| self.target.error = True |
| self.makefile.error = True |
| |
| assert self.currunning |
| self.currunning = False |
| self.runnext() |
| |
| class RemakeRuleContext(object): |
| def __init__(self, target, makefile, rule, deps, |
| targetstack, avoidremakeloop): |
| self.target = target |
| self.makefile = makefile |
| self.rule = rule |
| self.deps = deps |
| self.targetstack = targetstack |
| self.avoidremakeloop = avoidremakeloop |
| |
| self.running = False |
| self.error = False |
| self.depsremaining = len(deps) + 1 |
| self.remake = False |
| |
| def resolvedeps(self, serial, cb): |
| self.resolvecb = cb |
| self.didanything = False |
| if serial: |
| self._resolvedepsserial() |
| else: |
| self._resolvedepsparallel() |
| |
| def _weakdepfinishedserial(self, error, didanything): |
| if error: |
| self.remake = True |
| self._depfinishedserial(False, didanything) |
| |
| def _depfinishedserial(self, error, didanything): |
| assert error in (True, False) |
| |
| if didanything: |
| self.didanything = True |
| |
| if error: |
| self.error = True |
| if not self.makefile.keepgoing: |
| self.resolvecb(error=True, didanything=self.didanything) |
| return |
| |
| if len(self.resolvelist): |
| dep, weak = self.resolvelist.pop(0) |
| self.makefile.context.defer(dep.make, |
| self.makefile, self.targetstack, weak and self._weakdepfinishedserial or self._depfinishedserial) |
| else: |
| self.resolvecb(error=self.error, didanything=self.didanything) |
| |
| def _resolvedepsserial(self): |
| self.resolvelist = list(self.deps) |
| self._depfinishedserial(False, False) |
| |
| def _startdepparallel(self, d): |
| dep, weak = d |
| if weak: |
| depfinished = self._weakdepfinishedparallel |
| else: |
| depfinished = self._depfinishedparallel |
| if self.makefile.error: |
| depfinished(True, False) |
| else: |
| dep.make(self.makefile, self.targetstack, depfinished) |
| |
| def _weakdepfinishedparallel(self, error, didanything): |
| if error: |
| self.remake = True |
| self._depfinishedparallel(False, didanything) |
| |
| def _depfinishedparallel(self, error, didanything): |
| assert error in (True, False) |
| |
| if error: |
| print "<%s>: Found error" % self.target.target |
| self.error = True |
| if didanything: |
| self.didanything = True |
| |
| self.depsremaining -= 1 |
| if self.depsremaining == 0: |
| self.resolvecb(error=self.error, didanything=self.didanything) |
| |
| def _resolvedepsparallel(self): |
| self.depsremaining -= 1 |
| if self.depsremaining == 0: |
| self.resolvecb(error=self.error, didanything=self.didanything) |
| return |
| |
| self.didanything = False |
| |
| for d in self.deps: |
| self.makefile.context.defer(self._startdepparallel, d) |
| |
| def _commandcb(self, error): |
| assert error in (True, False) |
| |
| if error: |
| self.runcb(error=True) |
| return |
| |
| if len(self.commands): |
| self.commands.pop(0)(self._commandcb) |
| else: |
| self.runcb(error=False) |
| |
| def runcommands(self, indent, cb): |
| assert not self.running |
| self.running = True |
| |
| self.runcb = cb |
| |
| if self.rule is None or not len(self.rule.commands): |
| if self.target.mtime is None: |
| self.target.beingremade() |
| else: |
| for d, weak in self.deps: |
| if mtimeislater(d.mtime, self.target.mtime): |
| if d.mtime is None: |
| self.target.beingremade() |
| else: |
| _log.info("%sNot remaking %s ubecause it would have no effect, even though %s is newer.", indent, self.target.target, d.target) |
| break |
| cb(error=False) |
| return |
| |
| if self.rule.doublecolon: |
| if len(self.deps) == 0: |
| if self.avoidremakeloop: |
| _log.info("%sNot remaking %s using rule at %s because it would introduce an infinite loop.", indent, self.target.target, self.rule.loc) |
| cb(error=False) |
| return |
| |
| remake = self.remake |
| if remake: |
| _log.info("%sRemaking %s using rule at %s: weak dependency was not found.", indent, self.target.target, self.rule.loc) |
| else: |
| if self.target.mtime is None: |
| remake = True |
| _log.info("%sRemaking %s using rule at %s: target doesn't exist or is a forced target", indent, self.target.target, self.rule.loc) |
| |
| if not remake: |
| if self.rule.doublecolon: |
| if len(self.deps) == 0: |
| _log.info("%sRemaking %s using rule at %s because there are no prerequisites listed for a double-colon rule.", indent, self.target.target, self.rule.loc) |
| remake = True |
| |
| if not remake: |
| for d, weak in self.deps: |
| if mtimeislater(d.mtime, self.target.mtime): |
| _log.info("%sRemaking %s using rule at %s because %s is newer.", indent, self.target.target, self.rule.loc, d.target) |
| remake = True |
| break |
| |
| if remake: |
| self.target.beingremade() |
| self.target.didanything = True |
| try: |
| self.commands = [c for c in self.rule.getcommands(self.target, self.makefile)] |
| except util.MakeError, e: |
| print e |
| sys.stdout.flush() |
| cb(error=True) |
| return |
| |
| self._commandcb(False) |
| else: |
| cb(error=False) |
| |
| MAKESTATE_NONE = 0 |
| MAKESTATE_FINISHED = 1 |
| MAKESTATE_WORKING = 2 |
| |
| class Target(object): |
| """ |
| An actual (non-pattern) target. |
| |
| It holds target-specific variables and a list of rules. It may also point to a parent |
| PatternTarget, if this target is being created by an implicit rule. |
| |
| The rules associated with this target may be Rule instances or, in the case of static pattern |
| rules, PatternRule instances. |
| """ |
| |
| wasremade = False |
| |
| def __init__(self, target, makefile): |
| assert isinstance(target, str_type) |
| self.target = target |
| self.vpathtarget = None |
| self.rules = [] |
| self.variables = Variables(makefile.variables) |
| self.explicit = False |
| self._state = MAKESTATE_NONE |
| |
| def addrule(self, rule): |
| assert isinstance(rule, (Rule, PatternRuleInstance)) |
| if len(self.rules) and rule.doublecolon != self.rules[0].doublecolon: |
| raise DataError("Cannot have single- and double-colon rules for the same target. Prior rule location: %s" % self.rules[0].loc, rule.loc) |
| |
| if isinstance(rule, PatternRuleInstance): |
| if len(rule.prule.targetpatterns) != 1: |
| raise DataError("Static pattern rules must only have one target pattern", rule.prule.loc) |
| if rule.prule.targetpatterns[0].match(self.target) is None: |
| raise DataError("Static pattern rule doesn't match target '%s'" % self.target, rule.loc) |
| |
| self.rules.append(rule) |
| |
| def isdoublecolon(self): |
| return self.rules[0].doublecolon |
| |
| def isphony(self, makefile): |
| """Is this a phony target? We don't check for existence of phony targets.""" |
| return makefile.gettarget('.PHONY').hasdependency(self.target) |
| |
| def hasdependency(self, t): |
| for rule in self.rules: |
| if t in rule.prerequisites: |
| return True |
| |
| return False |
| |
| def resolveimplicitrule(self, makefile, targetstack, rulestack): |
| """ |
| Try to resolve an implicit rule to build this target. |
| """ |
| # The steps in the GNU make manual Implicit-Rule-Search.html are very detailed. I hope they can be trusted. |
| |
| indent = getindent(targetstack) |
| |
| _log.info("%sSearching for implicit rule to make '%s'", indent, self.target) |
| |
| dir, s, file = util.strrpartition(self.target, '/') |
| dir = dir + s |
| |
| candidates = [] # list of PatternRuleInstance |
| |
| hasmatch = util.any((r.hasspecificmatch(file) for r in makefile.implicitrules)) |
| |
| for r in makefile.implicitrules: |
| if r in rulestack: |
| _log.info("%s %s: Avoiding implicit rule recursion", indent, r.loc) |
| continue |
| |
| if not len(r.commands): |
| continue |
| |
| for ri in r.matchesfor(dir, file, hasmatch): |
| candidates.append(ri) |
| |
| newcandidates = [] |
| |
| for r in candidates: |
| depfailed = None |
| for p in r.prerequisites: |
| t = makefile.gettarget(p) |
| t.resolvevpath(makefile) |
| if not t.explicit and t.mtime is None: |
| depfailed = p |
| break |
| |
| if depfailed is not None: |
| if r.doublecolon: |
| _log.info("%s Terminal rule at %s doesn't match: prerequisite '%s' not mentioned and doesn't exist.", indent, r.loc, depfailed) |
| else: |
| newcandidates.append(r) |
| continue |
| |
| _log.info("%sFound implicit rule at %s for target '%s'", indent, r.loc, self.target) |
| self.rules.append(r) |
| return |
| |
| # Try again, but this time with chaining and without terminal (double-colon) rules |
| |
| for r in newcandidates: |
| newrulestack = rulestack + [r.prule] |
| |
| depfailed = None |
| for p in r.prerequisites: |
| t = makefile.gettarget(p) |
| try: |
| t.resolvedeps(makefile, targetstack, newrulestack, True) |
| except ResolutionError: |
| depfailed = p |
| break |
| |
| if depfailed is not None: |
| _log.info("%s Rule at %s doesn't match: prerequisite '%s' could not be made.", indent, r.loc, depfailed) |
| continue |
| |
| _log.info("%sFound implicit rule at %s for target '%s'", indent, r.loc, self.target) |
| self.rules.append(r) |
| return |
| |
| _log.info("%sCouldn't find implicit rule to remake '%s'", indent, self.target) |
| |
| def ruleswithcommands(self): |
| "The number of rules with commands" |
| return reduce(lambda i, rule: i + (len(rule.commands) > 0), self.rules, 0) |
| |
| def resolvedeps(self, makefile, targetstack, rulestack, recursive): |
| """ |
| Resolve the actual path of this target, using vpath if necessary. |
| |
| Recursively resolve dependencies of this target. This means finding implicit |
| rules which match the target, if appropriate. |
| |
| Figure out whether this target needs to be rebuild, and set self.outofdate |
| appropriately. |
| |
| @param targetstack is the current stack of dependencies being resolved. If |
| this target is already in targetstack, bail to prevent infinite |
| recursion. |
| @param rulestack is the current stack of implicit rules being used to resolve |
| dependencies. A rule chain cannot use the same implicit rule twice. |
| """ |
| assert makefile.parsingfinished |
| |
| if self.target in targetstack: |
| raise ResolutionError("Recursive dependency: %s -> %s" % ( |
| " -> ".join(targetstack), self.target)) |
| |
| targetstack = targetstack + [self.target] |
| |
| indent = getindent(targetstack) |
| |
| _log.info("%sConsidering target '%s'", indent, self.target) |
| |
| self.resolvevpath(makefile) |
| |
| # Sanity-check our rules. If we're single-colon, only one rule should have commands |
| ruleswithcommands = self.ruleswithcommands() |
| if len(self.rules) and not self.isdoublecolon(): |
| if ruleswithcommands > 1: |
| # In GNU make this is a warning, not an error. I'm going to be stricter. |
| # TODO: provide locations |
| raise DataError("Target '%s' has multiple rules with commands." % self.target) |
| |
| if ruleswithcommands == 0: |
| self.resolveimplicitrule(makefile, targetstack, rulestack) |
| |
| # If a target is mentioned, but doesn't exist, has no commands and no |
| # prerequisites, it is special and exists just to say that targets which |
| # depend on it are always out of date. This is like .FORCE but more |
| # compatible with other makes. |
| # Otherwise, we don't know how to make it. |
| if not len(self.rules) and self.mtime is None and not util.any((len(rule.prerequisites) > 0 |
| for rule in self.rules)): |
| raise ResolutionError("No rule to make target '%s' needed by %r" % (self.target, |
| targetstack)) |
| |
| if recursive: |
| for r in self.rules: |
| newrulestack = rulestack + [r] |
| for d in r.prerequisites: |
| dt = makefile.gettarget(d) |
| if dt.explicit: |
| continue |
| |
| dt.resolvedeps(makefile, targetstack, newrulestack, True) |
| |
| for v in makefile.getpatternvariablesfor(self.target): |
| self.variables.merge(v) |
| |
| def resolvevpath(self, makefile): |
| if self.vpathtarget is not None: |
| return |
| |
| if self.isphony(makefile): |
| self.vpathtarget = self.target |
| self.mtime = None |
| return |
| |
| if self.target.startswith('-l'): |
| stem = self.target[2:] |
| f, s, e = makefile.variables.get('.LIBPATTERNS') |
| if e is not None: |
| libpatterns = [Pattern(stripdotslash(s)) for s in e.resolvesplit(makefile, makefile.variables)] |
| if len(libpatterns): |
| searchdirs = [''] |
| searchdirs.extend(makefile.getvpath(self.target)) |
| |
| for lp in libpatterns: |
| if not lp.ispattern(): |
| raise DataError('.LIBPATTERNS contains a non-pattern') |
| |
| libname = lp.resolve('', stem) |
| |
| for dir in searchdirs: |
| libpath = util.normaljoin(dir, libname).replace('\\', '/') |
| fspath = util.normaljoin(makefile.workdir, libpath) |
| mtime = getmtime(fspath) |
| if mtime is not None: |
| self.vpathtarget = libpath |
| self.mtime = mtime |
| return |
| |
| self.vpathtarget = self.target |
| self.mtime = None |
| return |
| |
| search = [self.target] |
| if not os.path.isabs(self.target): |
| search += [util.normaljoin(dir, self.target).replace('\\', '/') |
| for dir in makefile.getvpath(self.target)] |
| |
| targetandtime = self.searchinlocs(makefile, search) |
| if targetandtime is not None: |
| (self.vpathtarget, self.mtime) = targetandtime |
| return |
| |
| self.vpathtarget = self.target |
| self.mtime = None |
| |
| def searchinlocs(self, makefile, locs): |
| """ |
| Look in the given locations relative to the makefile working directory |
| for a file. Return a pair of the target and the mtime if found, None |
| if not. |
| """ |
| for t in locs: |
| fspath = util.normaljoin(makefile.workdir, t).replace('\\', '/') |
| mtime = getmtime(fspath) |
| # _log.info("Searching %s ... checking %s ... mtime %r" % (t, fspath, mtime)) |
| if mtime is not None: |
| return (t, mtime) |
| |
| return None |
| |
| def beingremade(self): |
| """ |
| When we remake ourself, we have to drop any vpath prefixes. |
| """ |
| self.vpathtarget = self.target |
| self.wasremade = True |
| |
| def notifydone(self, makefile): |
| assert self._state == MAKESTATE_WORKING, "State was %s" % self._state |
| # If we were remade then resolve mtime again |
| if self.wasremade: |
| targetandtime = self.searchinlocs(makefile, [self.target]) |
| if targetandtime is not None: |
| (_, self.mtime) = targetandtime |
| else: |
| self.mtime = None |
| |
| self._state = MAKESTATE_FINISHED |
| for cb in self._callbacks: |
| makefile.context.defer(cb, error=self.error, didanything=self.didanything) |
| del self._callbacks |
| |
| def make(self, makefile, targetstack, cb, avoidremakeloop=False, printerror=True): |
| """ |
| If we are out of date, asynchronously make ourself. This is a multi-stage process, mostly handled |
| by the helper objects RemakeTargetSerially, RemakeTargetParallel, |
| RemakeRuleContext. These helper objects should keep us from developing |
| any cyclical dependencies. |
| |
| * resolve dependencies (synchronous) |
| * gather a list of rules to execute and related dependencies (synchronous) |
| * for each rule (in parallel) |
| ** remake dependencies (asynchronous) |
| ** build list of commands to execute (synchronous) |
| ** execute each command (asynchronous) |
| * asynchronously notify when all rules are complete |
| |
| @param cb A callback function to notify when remaking is finished. It is called |
| thusly: callback(error=True/False, didanything=True/False) |
| If there is no asynchronous activity to perform, the callback may be called directly. |
| """ |
| |
| serial = makefile.context.jcount == 1 |
| |
| if self._state == MAKESTATE_FINISHED: |
| cb(error=self.error, didanything=self.didanything) |
| return |
| |
| if self._state == MAKESTATE_WORKING: |
| assert not serial |
| self._callbacks.append(cb) |
| return |
| |
| assert self._state == MAKESTATE_NONE |
| |
| self._state = MAKESTATE_WORKING |
| self._callbacks = [cb] |
| self.error = False |
| self.didanything = False |
| |
| indent = getindent(targetstack) |
| |
| try: |
| self.resolvedeps(makefile, targetstack, [], False) |
| except util.MakeError, e: |
| if printerror: |
| print e |
| self.error = True |
| self.notifydone(makefile) |
| return |
| |
| assert self.vpathtarget is not None, "Target was never resolved!" |
| if not len(self.rules): |
| self.notifydone(makefile) |
| return |
| |
| if self.isdoublecolon(): |
| rulelist = [RemakeRuleContext(self, makefile, r, [(makefile.gettarget(p), False) for p in r.prerequisites], targetstack, avoidremakeloop) for r in self.rules] |
| else: |
| alldeps = [] |
| |
| commandrule = None |
| for r in self.rules: |
| rdeps = [(makefile.gettarget(p), r.weakdeps) for p in r.prerequisites] |
| if len(r.commands): |
| assert commandrule is None |
| commandrule = r |
| # The dependencies of the command rule are resolved before other dependencies, |
| # no matter the ordering of the other no-command rules |
| alldeps[0:0] = rdeps |
| else: |
| alldeps.extend(rdeps) |
| |
| rulelist = [RemakeRuleContext(self, makefile, commandrule, alldeps, targetstack, avoidremakeloop)] |
| |
| targetstack = targetstack + [self.target] |
| |
| if serial: |
| RemakeTargetSerially(self, makefile, indent, rulelist) |
| else: |
| RemakeTargetParallel(self, makefile, indent, rulelist) |
| |
| def dirpart(p): |
| d, s, f = util.strrpartition(p, '/') |
| if d == '': |
| return '.' |
| |
| return d |
| |
| def filepart(p): |
| d, s, f = util.strrpartition(p, '/') |
| return f |
| |
| def setautomatic(v, name, plist): |
| v.set(name, Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC, ' '.join(plist)) |
| v.set(name + 'D', Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC, ' '.join((dirpart(p) for p in plist))) |
| v.set(name + 'F', Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC, ' '.join((filepart(p) for p in plist))) |
| |
| def setautomaticvariables(v, makefile, target, prerequisites): |
| prtargets = [makefile.gettarget(p) for p in prerequisites] |
| prall = [pt.vpathtarget for pt in prtargets] |
| proutofdate = [pt.vpathtarget for pt in withoutdups(prtargets) |
| if target.mtime is None or mtimeislater(pt.mtime, target.mtime)] |
| |
| setautomatic(v, '@', [target.vpathtarget]) |
| if len(prall): |
| setautomatic(v, '<', [prall[0]]) |
| |
| setautomatic(v, '?', proutofdate) |
| setautomatic(v, '^', list(withoutdups(prall))) |
| setautomatic(v, '+', prall) |
| |
| def splitcommand(command): |
| """ |
| Using the esoteric rules, split command lines by unescaped newlines. |
| """ |
| start = 0 |
| i = 0 |
| while i < len(command): |
| c = command[i] |
| if c == '\\': |
| i += 1 |
| elif c == '\n': |
| yield command[start:i] |
| i += 1 |
| start = i |
| continue |
| |
| i += 1 |
| |
| if i > start: |
| yield command[start:i] |
| |
| def findmodifiers(command): |
| """ |
| Find any of +-@% prefixed on the command. |
| @returns (command, isHidden, isRecursive, ignoreErrors, isNative) |
| """ |
| |
| isHidden = False |
| isRecursive = False |
| ignoreErrors = False |
| isNative = False |
| |
| realcommand = command.lstrip(' \t\n@+-%') |
| modset = set(command[:-len(realcommand)]) |
| return realcommand, '@' in modset, '+' in modset, '-' in modset, '%' in modset |
| |
| class _CommandWrapper(object): |
| def __init__(self, cline, ignoreErrors, loc, context, **kwargs): |
| self.ignoreErrors = ignoreErrors |
| self.loc = loc |
| self.cline = cline |
| self.kwargs = kwargs |
| self.context = context |
| |
| def _cb(self, res): |
| if res != 0 and not self.ignoreErrors: |
| print "%s: command '%s' failed, return code %i" % (self.loc, self.cline, res) |
| self.usercb(error=True) |
| else: |
| self.usercb(error=False) |
| |
| def __call__(self, cb): |
| self.usercb = cb |
| process.call(self.cline, loc=self.loc, cb=self._cb, context=self.context, **self.kwargs) |
| |
| class _NativeWrapper(_CommandWrapper): |
| def __init__(self, cline, ignoreErrors, loc, context, |
| pycommandpath, **kwargs): |
| _CommandWrapper.__init__(self, cline, ignoreErrors, loc, context, |
| **kwargs) |
| if pycommandpath: |
| self.pycommandpath = re.split('[%s\s]+' % os.pathsep, |
| pycommandpath) |
| else: |
| self.pycommandpath = None |
| |
| def __call__(self, cb): |
| # get the module and method to call |
| parts, badchar = process.clinetoargv(self.cline, self.kwargs['cwd']) |
| if parts is None: |
| raise DataError("native command '%s': shell metacharacter '%s' in command line" % (self.cline, badchar), self.loc) |
| if len(parts) < 2: |
| raise DataError("native command '%s': no method name specified" % self.cline, self.loc) |
| module = parts[0] |
| method = parts[1] |
| cline_list = parts[2:] |
| self.usercb = cb |
| process.call_native(module, method, cline_list, |
| loc=self.loc, cb=self._cb, context=self.context, |
| pycommandpath=self.pycommandpath, **self.kwargs) |
| |
| def getcommandsforrule(rule, target, makefile, prerequisites, stem): |
| v = Variables(parent=target.variables) |
| setautomaticvariables(v, makefile, target, prerequisites) |
| if stem is not None: |
| setautomatic(v, '*', [stem]) |
| |
| env = makefile.getsubenvironment(v) |
| |
| for c in rule.commands: |
| cstring = c.resolvestr(makefile, v) |
| for cline in splitcommand(cstring): |
| cline, isHidden, isRecursive, ignoreErrors, isNative = findmodifiers(cline) |
| if (isHidden or makefile.silent) and not makefile.justprint: |
| echo = None |
| else: |
| echo = "%s$ %s" % (c.loc, cline) |
| if not isNative: |
| yield _CommandWrapper(cline, ignoreErrors=ignoreErrors, env=env, cwd=makefile.workdir, loc=c.loc, context=makefile.context, |
| echo=echo, justprint=makefile.justprint) |
| else: |
| f, s, e = v.get("PYCOMMANDPATH", True) |
| if e: |
| e = e.resolvestr(makefile, v, ["PYCOMMANDPATH"]) |
| yield _NativeWrapper(cline, ignoreErrors=ignoreErrors, |
| env=env, cwd=makefile.workdir, |
| loc=c.loc, context=makefile.context, |
| echo=echo, justprint=makefile.justprint, |
| pycommandpath=e) |
| |
| class Rule(object): |
| """ |
| A rule contains a list of prerequisites and a list of commands. It may also |
| contain rule-specific variables. This rule may be associated with multiple targets. |
| """ |
| |
| def __init__(self, prereqs, doublecolon, loc, weakdeps): |
| self.prerequisites = prereqs |
| self.doublecolon = doublecolon |
| self.commands = [] |
| self.loc = loc |
| self.weakdeps = weakdeps |
| |
| def addcommand(self, c): |
| assert isinstance(c, (Expansion, StringExpansion)) |
| self.commands.append(c) |
| |
| def getcommands(self, target, makefile): |
| assert isinstance(target, Target) |
| # Prerequisites are merged if the target contains multiple rules and is |
| # not a terminal (double colon) rule. See |
| # https://www.gnu.org/software/make/manual/make.html#Multiple-Targets. |
| prereqs = [] |
| prereqs.extend(self.prerequisites) |
| |
| if not self.doublecolon: |
| for rule in target.rules: |
| # The current rule comes first, which is already in prereqs so |
| # we don't need to add it again. |
| if rule != self: |
| prereqs.extend(rule.prerequisites) |
| |
| return getcommandsforrule(self, target, makefile, prereqs, stem=None) |
| # TODO: $* in non-pattern rules? |
| |
| class PatternRuleInstance(object): |
| weakdeps = False |
| |
| """ |
| A pattern rule instantiated for a particular target. It has the same API as Rule, but |
| different internals, forwarding most information on to the PatternRule. |
| """ |
| def __init__(self, prule, dir, stem, ismatchany): |
| assert isinstance(prule, PatternRule) |
| |
| self.dir = dir |
| self.stem = stem |
| self.prule = prule |
| self.prerequisites = prule.prerequisitesforstem(dir, stem) |
| self.doublecolon = prule.doublecolon |
| self.loc = prule.loc |
| self.ismatchany = ismatchany |
| self.commands = prule.commands |
| |
| def getcommands(self, target, makefile): |
| assert isinstance(target, Target) |
| return getcommandsforrule(self, target, makefile, self.prerequisites, stem=self.dir + self.stem) |
| |
| def __str__(self): |
| return "Pattern rule at %s with stem '%s', matchany: %s doublecolon: %s" % (self.loc, |
| self.dir + self.stem, |
| self.ismatchany, |
| self.doublecolon) |
| |
| class PatternRule(object): |
| """ |
| An implicit rule or static pattern rule containing target patterns, prerequisite patterns, |
| and a list of commands. |
| """ |
| |
| def __init__(self, targetpatterns, prerequisites, doublecolon, loc): |
| self.targetpatterns = targetpatterns |
| self.prerequisites = prerequisites |
| self.doublecolon = doublecolon |
| self.loc = loc |
| self.commands = [] |
| |
| def addcommand(self, c): |
| assert isinstance(c, (Expansion, StringExpansion)) |
| self.commands.append(c) |
| |
| def ismatchany(self): |
| return util.any((t.ismatchany() for t in self.targetpatterns)) |
| |
| def hasspecificmatch(self, file): |
| for p in self.targetpatterns: |
| if not p.ismatchany() and p.match(file) is not None: |
| return True |
| |
| return False |
| |
| def matchesfor(self, dir, file, skipsinglecolonmatchany): |
| """ |
| Determine all the target patterns of this rule that might match target t. |
| @yields a PatternRuleInstance for each. |
| """ |
| |
| for p in self.targetpatterns: |
| matchany = p.ismatchany() |
| if matchany: |
| if skipsinglecolonmatchany and not self.doublecolon: |
| continue |
| |
| yield PatternRuleInstance(self, dir, file, True) |
| else: |
| stem = p.match(dir + file) |
| if stem is not None: |
| yield PatternRuleInstance(self, '', stem, False) |
| else: |
| stem = p.match(file) |
| if stem is not None: |
| yield PatternRuleInstance(self, dir, stem, False) |
| |
| def prerequisitesforstem(self, dir, stem): |
| return [p.resolve(dir, stem) for p in self.prerequisites] |
| |
| class _RemakeContext(object): |
| def __init__(self, makefile, cb): |
| self.makefile = makefile |
| self.included = [(makefile.gettarget(f), required) |
| for f, required in makefile.included] |
| self.toremake = list(self.included) |
| self.cb = cb |
| |
| self.remakecb(error=False, didanything=False) |
| |
| def remakecb(self, error, didanything): |
| assert error in (True, False) |
| |
| if error and self.required: |
| print "Error remaking makefiles (ignored)" |
| |
| if len(self.toremake): |
| target, self.required = self.toremake.pop(0) |
| target.make(self.makefile, [], avoidremakeloop=True, cb=self.remakecb, printerror=False) |
| else: |
| for t, required in self.included: |
| if t.wasremade: |
| _log.info("Included file %s was remade, restarting make", t.target) |
| self.cb(remade=True) |
| return |
| elif required and t.mtime is None: |
| self.cb(remade=False, error=DataError("No rule to remake missing include file %s" % t.target)) |
| return |
| |
| self.cb(remade=False) |
| |
| class Makefile(object): |
| """ |
| The top-level data structure for makefile execution. It holds Targets, implicit rules, and other |
| state data. |
| """ |
| |
| def __init__(self, workdir=None, env=None, restarts=0, make=None, |
| makeflags='', makeoverrides='', |
| makelevel=0, context=None, targets=(), keepgoing=False, |
| silent=False, justprint=False): |
| self.defaulttarget = None |
| |
| if env is None: |
| env = os.environ |
| self.env = env |
| |
| self.variables = Variables() |
| self.variables.readfromenvironment(env) |
| |
| self.context = context |
| self.exportedvars = {} |
| self._targets = {} |
| self.keepgoing = keepgoing |
| self.silent = silent |
| self.justprint = justprint |
| self._patternvariables = [] # of (pattern, variables) |
| self.implicitrules = [] |
| self.parsingfinished = False |
| |
| self._patternvpaths = [] # of (pattern, [dir, ...]) |
| |
| if workdir is None: |
| workdir = os.getcwd() |
| workdir = os.path.realpath(workdir) |
| self.workdir = workdir |
| self.variables.set('CURDIR', Variables.FLAVOR_SIMPLE, |
| Variables.SOURCE_AUTOMATIC, workdir.replace('\\','/')) |
| |
| # the list of included makefiles, whether or not they existed |
| self.included = [] |
| |
| self.variables.set('MAKE_RESTARTS', Variables.FLAVOR_SIMPLE, |
| Variables.SOURCE_AUTOMATIC, restarts > 0 and str(restarts) or '') |
| |
| self.variables.set('.PYMAKE', Variables.FLAVOR_SIMPLE, |
| Variables.SOURCE_MAKEFILE, "1") |
| if make is not None: |
| self.variables.set('MAKE', Variables.FLAVOR_SIMPLE, |
| Variables.SOURCE_MAKEFILE, make) |
| |
| if makeoverrides != '': |
| self.variables.set('-*-command-variables-*-', Variables.FLAVOR_SIMPLE, |
| Variables.SOURCE_AUTOMATIC, makeoverrides) |
| makeflags += ' -- $(MAKEOVERRIDES)' |
| |
| self.variables.set('MAKEOVERRIDES', Variables.FLAVOR_RECURSIVE, |
| Variables.SOURCE_ENVIRONMENT, |
| '${-*-command-variables-*-}') |
| |
| self.variables.set('MAKEFLAGS', Variables.FLAVOR_RECURSIVE, |
| Variables.SOURCE_MAKEFILE, makeflags) |
| self.exportedvars['MAKEFLAGS'] = True |
| |
| self.makelevel = makelevel |
| self.variables.set('MAKELEVEL', Variables.FLAVOR_SIMPLE, |
| Variables.SOURCE_MAKEFILE, str(makelevel)) |
| |
| self.variables.set('MAKECMDGOALS', Variables.FLAVOR_SIMPLE, |
| Variables.SOURCE_AUTOMATIC, ' '.join(targets)) |
| |
| for vname, val in implicit.variables.iteritems(): |
| self.variables.set(vname, |
| Variables.FLAVOR_SIMPLE, |
| Variables.SOURCE_IMPLICIT, val) |
| |
| def foundtarget(self, t): |
| """ |
| Inform the makefile of a target which is a candidate for being the default target, |
| if there isn't already a default target. |
| """ |
| flavor, source, value = self.variables.get('.DEFAULT_GOAL') |
| if self.defaulttarget is None and t != '.PHONY' and value is None: |
| self.defaulttarget = t |
| self.variables.set('.DEFAULT_GOAL', Variables.FLAVOR_SIMPLE, |
| Variables.SOURCE_AUTOMATIC, t) |
| |
| def getpatternvariables(self, pattern): |
| assert isinstance(pattern, Pattern) |
| |
| for p, v in self._patternvariables: |
| if p == pattern: |
| return v |
| |
| v = Variables() |
| self._patternvariables.append( (pattern, v) ) |
| return v |
| |
| def getpatternvariablesfor(self, target): |
| for p, v in self._patternvariables: |
| if p.match(target): |
| yield v |
| |
| def hastarget(self, target): |
| return target in self._targets |
| |
| _globcheck = re.compile('[[*?]') |
| def gettarget(self, target): |
| assert isinstance(target, str_type) |
| |
| target = target.rstrip('/') |
| |
| assert target != '', "empty target?" |
| |
| assert not self._globcheck.match(target) |
| |
| t = self._targets.get(target, None) |
| if t is None: |
| t = Target(target, self) |
| self._targets[target] = t |
| return t |
| |
| def appendimplicitrule(self, rule): |
| assert isinstance(rule, PatternRule) |
| self.implicitrules.append(rule) |
| |
| def finishparsing(self): |
| """ |
| Various activities, such as "eval", are not allowed after parsing is |
| finished. In addition, various warnings and errors can only be issued |
| after the parsing data model is complete. All dependency resolution |
| and rule execution requires that parsing be finished. |
| """ |
| self.parsingfinished = True |
| |
| flavor, source, value = self.variables.get('GPATH') |
| if value is not None and value.resolvestr(self, self.variables, ['GPATH']).strip() != '': |
| raise DataError('GPATH was set: pymake does not support GPATH semantics') |
| |
| flavor, source, value = self.variables.get('VPATH') |
| if value is None: |
| self._vpath = [] |
| else: |
| self._vpath = filter(lambda e: e != '', |
| re.split('[%s\s]+' % os.pathsep, |
| value.resolvestr(self, self.variables, ['VPATH']))) |
| |
| targets = list(self._targets.itervalues()) |
| for t in targets: |
| t.explicit = True |
| for r in t.rules: |
| for p in r.prerequisites: |
| self.gettarget(p).explicit = True |
| |
| np = self.gettarget('.NOTPARALLEL') |
| if len(np.rules): |
| self.context = process.getcontext(1) |
| |
| flavor, source, value = self.variables.get('.DEFAULT_GOAL') |
| if value is not None: |
| self.defaulttarget = value.resolvestr(self, self.variables, ['.DEFAULT_GOAL']).strip() |
| |
| self.error = False |
| |
| def include(self, path, required=True, weak=False, loc=None): |
| """ |
| Include the makefile at `path`. |
| """ |
| self.included.append((path, required)) |
| fspath = util.normaljoin(self.workdir, path) |
| if os.path.exists(fspath): |
| if weak: |
| stmts = parser.parsedepfile(fspath) |
| else: |
| stmts = parser.parsefile(fspath) |
| self.variables.append('MAKEFILE_LIST', Variables.SOURCE_AUTOMATIC, path, None, self) |
| stmts.execute(self, weak=weak) |
| self.gettarget(path).explicit = True |
| |
| def addvpath(self, pattern, dirs): |
| """ |
| Add a directory to the vpath search for the given pattern. |
| """ |
| self._patternvpaths.append((pattern, dirs)) |
| |
| def clearvpath(self, pattern): |
| """ |
| Clear vpaths for the given pattern. |
| """ |
| self._patternvpaths = [(p, dirs) |
| for p, dirs in self._patternvpaths |
| if not p.match(pattern)] |
| |
| def clearallvpaths(self): |
| self._patternvpaths = [] |
| |
| def getvpath(self, target): |
| vp = list(self._vpath) |
| for p, dirs in self._patternvpaths: |
| if p.match(target): |
| vp.extend(dirs) |
| |
| return withoutdups(vp) |
| |
| def remakemakefiles(self, cb): |
| mlist = [] |
| for f, required in self.included: |
| t = self.gettarget(f) |
| t.explicit = True |
| t.resolvevpath(self) |
| oldmtime = t.mtime |
| |
| mlist.append((t, oldmtime)) |
| |
| _RemakeContext(self, cb) |
| |
| def getsubenvironment(self, variables): |
| env = dict(self.env) |
| for vname, v in self.exportedvars.iteritems(): |
| if v: |
| flavor, source, val = variables.get(vname) |
| if val is None: |
| strval = '' |
| else: |
| strval = val.resolvestr(self, variables, [vname]) |
| env[vname] = strval |
| else: |
| env.pop(vname, None) |
| |
| makeflags = '' |
| |
| env['MAKELEVEL'] = str(self.makelevel + 1) |
| return env |