mirror of
https://github.com/wesnoth/wesnoth
synced 2025-05-07 11:12:04 +00:00
294 lines
13 KiB
Python
294 lines
13 KiB
Python
"""
|
|
wmltools.py -- Python routines for working with a Battle For Wesnoth WMl tree
|
|
|
|
"""
|
|
|
|
import sys, os, re, sre_constants, md5
|
|
|
|
resource_extensions = ("png", "jpg", "ogg", "wav", "map")
|
|
|
|
class Forest:
|
|
"Return an iterable directory forest object."
|
|
def __init__(self, dirpath, exclude=None):
|
|
"Get the names of all files under dirpath, ignoring .svn directories."
|
|
self.forest = []
|
|
self.dirpath = dirpath
|
|
for dir in dirpath:
|
|
subtree = []
|
|
if os.path.isdir(dir): # So we skip .cfgs in a UMC mirror
|
|
os.path.walk(dir,
|
|
lambda arg, dir, names: subtree.extend(map(lambda x: os.path.normpath(os.path.join(dir, x)), names)),
|
|
None)
|
|
self.forest.append(subtree)
|
|
for i in range(len(self.forest)):
|
|
self.forest[i] = filter(lambda x: ".svn" not in x, self.forest[i])
|
|
self.forest[i] = filter(lambda x: not os.path.isdir(x), self.forest[i])
|
|
if exclude:
|
|
self.forest[i] = filter(lambda x: not re.search(exclude, x), self.forest[i])
|
|
self.forest[i] = filter(lambda x: not x.endswith("-bak"), self.forest[i])
|
|
# Compute cliques (will be used later for visibility checks)
|
|
self.clique = {}
|
|
counter = 0
|
|
for tree in self.forest:
|
|
for filename in tree:
|
|
self.clique[filename] = counter
|
|
counter += 1
|
|
def parent(self, filename):
|
|
"Return the directory root that caused this path to be included."
|
|
return self.dirpath[self.clique[filename]]
|
|
def neighbors(self, fn1, fn2):
|
|
"Are two files from the same tree?"
|
|
return self.clique[fn1] == self.clique[fn2]
|
|
def flatten(self):
|
|
allfiles = []
|
|
for tree in self.forest:
|
|
allfiles += tree
|
|
return allfiles
|
|
def generator(self):
|
|
"Return a generator that walks through all files."
|
|
for tree in self.forest:
|
|
for filename in tree:
|
|
yield filename
|
|
|
|
def iswml(filename):
|
|
"Is the specified filename WML?"
|
|
return filename.endswith(".cfg")
|
|
|
|
def isresource(filename):
|
|
"Is the specifired name a resource?"
|
|
(root, ext) = os.path.splitext(filename)
|
|
return ext and ext[1:] in resource_extensions
|
|
|
|
class Reference:
|
|
"Describes a location by file and line."
|
|
def __init__(self, filename, lineno=None, docstring=None):
|
|
self.filename = filename
|
|
self.lineno = lineno
|
|
self.references = {}
|
|
self.docstring = docstring
|
|
self.undef = None
|
|
def append(self, fn, n):
|
|
if fn not in self.references:
|
|
self.references[fn] = []
|
|
self.references[fn].append(n+1)
|
|
def dump_references(self):
|
|
for (file, linenumbers) in self.references.items():
|
|
print " %s: %s" % (file, `linenumbers`[1:-1])
|
|
def __cmp__(self, other):
|
|
"Compare two documentation objects for place in the sort order."
|
|
# Major sort by file, minor by line number. This presumes that the
|
|
# files correspond to coherent topics and gives us control of the
|
|
# sequence.
|
|
byfile = cmp(self.filename, other.filename)
|
|
if byfile:
|
|
return byfile
|
|
else:
|
|
return cmp(self.lineno, other.lineno)
|
|
def __str__(self):
|
|
if self.lineno:
|
|
return '"%s", line %d' % (self.filename, self.lineno)
|
|
else:
|
|
return self.filename
|
|
__repr__ = __str__
|
|
|
|
class CrossRef:
|
|
macro_reference = re.compile(r"\{([A-Z_][A-Z0-9_:]*[A-Za-z0-9_])\b")
|
|
file_reference = re.compile(r"[A-Za-z0-9{}.][A-Za-z0-9_/+{}.-]*\.(" + "|".join(resource_extensions) + ")")
|
|
def mark_matching_resources(self, pattern, fn, n):
|
|
"Mark all definitions matching a specified pattern with a reference."
|
|
pattern = pattern.replace("+", r"\+")
|
|
try:
|
|
pattern = re.compile(os.sep + pattern + "$")
|
|
except sre_constants.error:
|
|
print >>sys.stderr, "macroscope: confused by %s" % pattern
|
|
return None
|
|
key = None
|
|
for trial in self.fileref:
|
|
if pattern.search(trial) and self.visible_from(trial, fn, n):
|
|
key = trial
|
|
self.fileref[key].append(fn, n)
|
|
return key
|
|
def visible_from(self, defn, fn, n):
|
|
"Is specified definition visible from the specified file and line?"
|
|
if type(defn) == type(""):
|
|
defn = self.fileref[defn]
|
|
if defn.undef != None:
|
|
# Local macros are only visible in the file where they were defined
|
|
return defn.filename == fn and n >= defn.lineno and n <= defn.undef
|
|
elif defn.filename in self.filelist.forest[0]:
|
|
# Macros and resources in the first subtree are visible everywhere.
|
|
return True
|
|
elif not self.filelist.neighbors(defn.filename, fn):
|
|
# Otherwise, must be in the same subtree.
|
|
return False
|
|
else:
|
|
# If the two files are in the same subtree, assume visibility.
|
|
# This doesn't match the actual preprocessor semantics.
|
|
# It means any macro without an undef is visible anywhere in the
|
|
# same argument directory.
|
|
#
|
|
# We can't do better than this without a lot of hairy graph-
|
|
# coloring logic to simulate include path interpretation.
|
|
# If that logic ever gets built, it will go here.
|
|
return True
|
|
def __init__(self, dirpath, exclude="", warnlevel=0):
|
|
"Build cross-reference object from the specified filelist."
|
|
self.dirpath = dirpath
|
|
self.filelist = Forest(dirpath, exclude)
|
|
self.xref = {}
|
|
self.fileref = {}
|
|
self.noxref = False
|
|
for filename in self.filelist.generator():
|
|
if warnlevel > 1:
|
|
print filename + ":"
|
|
if isresource(filename):
|
|
self.fileref[filename] = Reference(filename)
|
|
elif iswml(filename):
|
|
# It's a WML file, scan for macro defitions
|
|
dfp = open(filename)
|
|
state = "outside"
|
|
for (n, line) in enumerate(dfp):
|
|
if warnlevel > 1:
|
|
print `line`[1:-1]
|
|
if line.strip().startswith("#define"):
|
|
tokens = line.split()
|
|
name = tokens[1]
|
|
here = Reference(filename, n+1, line)
|
|
here.hash = md5.new()
|
|
here.docstring = line.lstrip()[8:] # Strip off #define_
|
|
state = "macro_header"
|
|
continue
|
|
elif state != 'outside' and line.strip().endswith("#enddef"):
|
|
here.hash.update(line)
|
|
here.hash = here.hash.digest()
|
|
if name in self.xref:
|
|
for defn in self.xref[name]:
|
|
if not self.visible_from(defn, filename, n):
|
|
continue
|
|
elif defn.hash != here.hash:
|
|
print >>sys.stderr, \
|
|
"%s: overrides different %s definition at %s" \
|
|
% (here, name, defn)
|
|
elif warnlevel > 0:
|
|
print >>sys.stderr, \
|
|
"%s: duplicates %s definition at %s" \
|
|
% (here, name, defn)
|
|
if name not in self.xref:
|
|
self.xref[name] = []
|
|
self.xref[name].append(here)
|
|
state = "outside"
|
|
elif state == "macro_header" and line and line[0] != "#":
|
|
state = "macro_body"
|
|
if state == "macro_header":
|
|
here.docstring += line[1:]
|
|
if state in ("macro_header", "macro_body"):
|
|
here.hash.update(line)
|
|
elif line.strip().startswith("#undef"):
|
|
tokens = line.split()
|
|
name = tokens[1]
|
|
if name in self.xref and self.xref[name]:
|
|
self.xref[name][-1].undef = n
|
|
else:
|
|
print "%s: unbalanced #undef on %s" \
|
|
% (Reference(filename, n), name)
|
|
dfp.close()
|
|
elif filename.endswith(".def"):
|
|
# It's a list of names to be considered defined
|
|
self.noxref = True
|
|
dfp = open(filename)
|
|
for line in dfp:
|
|
self.xref[line.strip()] = True
|
|
dfp.close()
|
|
|
|
# Next, decorate definitions with all references from the filelist.
|
|
self.unresolved = []
|
|
self.missing = []
|
|
formals = []
|
|
for fn in self.filelist.generator():
|
|
if iswml(fn):
|
|
rfp = open(fn)
|
|
for (n, line) in enumerate(rfp):
|
|
if line.startswith("#define"):
|
|
formals = line.split()[2:]
|
|
elif line.startswith("#enddef"):
|
|
formals = []
|
|
if '#' in line:
|
|
line = line.split('#')[0]
|
|
if not line:
|
|
continue
|
|
# Find references to macros
|
|
for match in re.finditer(CrossRef.macro_reference, line):
|
|
name = match.group(1)
|
|
candidates = 0
|
|
if name in formals:
|
|
continue
|
|
elif name in self.xref:
|
|
for defn in self.xref[name]:
|
|
if self.visible_from(defn, fn, n+1):
|
|
candidates += 1
|
|
defn.append(fn, n+1)
|
|
if candidates > 1:
|
|
print "%s: more than one definition of %s is visible here." % (Reference(fn, n), name)
|
|
if candidates == 0:
|
|
self.unresolved.append((name, Reference(fn,n+1)))
|
|
# Find references to resource files
|
|
for match in re.finditer(CrossRef.file_reference, line):
|
|
name = match.group(0)
|
|
# Catches maps that look like macro names.
|
|
if name.endswith(".map") and name[0] == '{':
|
|
name = name[1:]
|
|
key = None
|
|
# If name is already in our resource list, it's easy.
|
|
if name in self.fileref and self.visible_from(name, fn, n):
|
|
self.fileref[trial].append(fn, n+1)
|
|
continue
|
|
# If the name contains subtitutable parts, count
|
|
# it as a reference to everything the substitutions
|
|
# could potentially match.
|
|
elif '{' in name:
|
|
pattern = re.sub(r"\{[^}]*\}", '.*', name)
|
|
key = self.mark_matching_resources(pattern, fn,n+1)
|
|
if key:
|
|
self.fileref[key].append(fn, n+1)
|
|
else:
|
|
candidates = []
|
|
for trial in self.fileref:
|
|
if trial.endswith(os.sep + name) and self.visible_from(trial, fn, n):
|
|
key = trial
|
|
self.fileref[trial].append(fn, n+1)
|
|
candidates.append(trial)
|
|
if len(candidates) > 1:
|
|
print "%s: more than one definition of %s is visible here (%s)." % (Reference(fn, n), name, ", ".join(candidates))
|
|
if not key:
|
|
self.missing.append((name, Reference(fn,n+1)))
|
|
rfp.close()
|
|
|
|
## Version-control hooks begin here.
|
|
#
|
|
# Change these if we move away from Subversion
|
|
|
|
vcdir = ".svn"
|
|
|
|
def vcmove(src, dst):
|
|
"Move a file under version control. Only applied to unmodified files."
|
|
(dir, base) = os.path.split(src)
|
|
if os.path.exists(os.path.join(dir, ".svn")):
|
|
return "svn mv %s %s" % (src, dst)
|
|
else:
|
|
return "mv %s %s" % (src, dst)
|
|
|
|
def vcunmove(src, dst):
|
|
"Revert the result of a previous move (before commit)."
|
|
(dir, base) = os.path.split(src)
|
|
if os.path.exists(os.path.join(dir, ".svn")):
|
|
return "svn revert %s" % dst # Revert the add at the destination
|
|
return "rm %s" dst # Remove the moved copy
|
|
return "svn revert %s" % src # Revert the deletion
|
|
else:
|
|
return "mv %s %s" % (dst, src)
|
|
|
|
#
|
|
## Version-control hooks end here
|
|
|
|
# wmltools.py ends here
|