# GARStow utility library # Copyright 2003, 2004, 2005, 2006, 2007 Adam Sampson # To do: # - check that package names passed are valid packages # - get dirs from gar.conf.mk (via a print rule) # - check for circular dependencies import sys, os, stat, re, StringIO, copy, commands gar_dir = os.path.normpath(os.path.join(sys.path[0], "..")) def warn(*args): print >>sys.stderr, "".join(map(str, args)) def die(*args): warn(*args) sys.exit(1) base_rules = """ include """ + gar_dir + """/gar.conf.mk GARDIR ?= ../.. FILEDIR ?= files DOWNLOADDIR ?= download COOKIEDIR ?= cookies WORKDIR ?= work WORKSRC ?= $(WORKDIR)/$(DISTNAME) WORKOBJ ?= $(WORKSRC) EXTRACTDIR ?= $(WORKDIR) SCRATCHDIR ?= tmp CHECKSUM_FILE ?= checksums MANIFEST_FILE ?= manifest UPSTREAMNAME ?= $(GARNAME) DISTNAME ?= $(UPSTREAMNAME)-$(GARVERSION) PACKAGENAME ?= $(GARNAME)-$(GARVERSION) packageprefix = $(packagesdir)/$(PACKAGENAME) packagedocs = $(packageprefix)/share/doc/$(GARNAME) """ class PortfileError(Exception): pass def parse_portfile(f): """Parse the restricted subset of GNU Make syntax allowed in GARStow Makefiles.""" s = "" lines = [] for l in f.readlines(): l = l[:-1] # Fold together continuations if len(l) > 0 and l[-1] == "\\": s += l[:-1] else: lines.append(s + l) s = "" parsed = [] n = 0 max = len(lines) while n < max: l = re.sub(r'\s+$', '', lines[n]) # Remove comments l = re.sub(r'\s*#.*$', '', l) # Skip comments and blank lines if l == "": n += 1 continue # Variable definitions m = re.match(r'^(\S+)\s*(=|\+=|:=|\?=)\s*(.*)$', l) if m is not None: kinds = {"=": "var", "+=": "varadd", ":=": "varsimple", "?=": "varmaybe"} parsed.append((kinds[m.group(2)], m.group(1), m.group(3))) n += 1 continue # Long-form variable definitions m = re.match(r'^define\s*(\S+)$', l) if m is not None: name = m.group(1) content = "" while n < max: n += 1 if lines[n] == "endef": n += 1 break content += lines[n] + "\n" parsed.append(("var", name, content)) continue # Inclusions and exports m = re.match(r'^(include|export|unexport)\s*(.*)$', l) if m is not None: parsed.append((m.group(1), m.group(2))) n += 1 continue # Rules m = re.match(r'^(\S.*):(\s*.+)?$', l) if m is not None: name = m.group(1) n += 1 commands = [] while n < max: if lines[n].strip() == "": n += 1 continue m = re.match(r'^\t(.*)$', lines[n]) if m is None: break commands.append(m.group(1)) n += 1 parsed.append(("rule", name, commands)) continue raise PortfileError("Unrecognised line: " + l) return parsed def split_whitespace(s): return re.split(r'\s+', s) class Portfile: def __init__(self, fn, **keys): self.vars = {} self.vartypes = {} self.varsources = {} self.rules = {} self.exports = {} self.orig_environ = copy.copy(os.environ) self.load_errors = [] self.load(fn, **keys) self.filename = fn def expand(self, s): obits = [] o = 0 max = len(s) while 1: i = s.find('$', o) if i == -1: obits.append(s[o:]) break obits.append(s[o:i]) if i + 1 >= max: raise PortfileError("Incomplete $ sequence in: " + s) c = s[i + 1] if c == "$": # $$ obits.append("$") o = i + 2 continue elif c == "(": # $(v) level = 1 i += 2 start = i while level > 0: nextl = s.find("(", i) nextr = s.find(")", i) if nextr == -1: raise PortfileError("Mismatched brackets in: " + s) elif nextl != -1 and nextl < nextr: level += 1 i = nextl + 1 else: level -= 1 i = nextr + 1 wanted = s[start:i - 1] o = i else: # $v wanted = c o = i + 2 wanted = self.expand(wanted) i = wanted.find(" ") if i == -1: obits.append(self.variable(wanted)) else: obits.append(self.function(wanted[:i], wanted[i + 1:])) return "".join(obits) def variable(self, name): if name in self.vars: return self.expand(self.vars[name]) elif name in os.environ: return os.environ[name] else: return "" def set_environ(self): for e, v in self.orig_environ.items(): os.environ[e] = v for e in os.environ.keys(): if not e in self.orig_environ: del os.environ[e] for e in self.exports.keys(): os.environ[e] = self.variable(e) def function(self, name, args): if name == "wildcard": name = "shell" args = "echo " + args if name == "shell": self.set_environ() v = commands.getoutput(args) v = v.replace("\r\n", " ") v = v.replace("\n", " ") if v != "" and v[-1] == "\n": return v[:-1] else: return v elif name == "subst": as = args.split(",", 2) if len(as) < 3: raise PortfileError("Too few arguments to subst") return as[2].replace(as[0], as[1]) elif name == "addsuffix": as = args.split(",", 1) if len(as) < 2: raise PortfileError("Too few arguments to addsuffix") return " ".join([x + as[0] for x in split_whitespace(as[1])]) elif name == "if": as = args.split(",", 1) if len(as) < 2: raise PortfileError("Too few arguments to if") if as[0].strip() != "": return as[1] elif len(as) == 3: return as[2] else: return "" else: raise PortfileError("Unknown function " + name) var_order = ["GARNAME", "GARVERSION", "CATEGORIES", "MASTER_SITES", "MASTER_SUBDIR", "DISTFILE_SITES", "DISTFILE_SUBDIR", "SIGFILE_SITES", "SIGFILE_SUBDIR", "PATCHFILE_SITES", "PATCHFILE_SUBDIR", "UPSTREAMNAME", "DISTNAME", "DISTFILES", "SIGFILES", "PATCHFILES", "NOCHECKSUM", "PATCHOPTS", "PATCHDIR", "LIBDEPS", "BUILDDEPS", "WORKSRC", "WORKOBJ", "DESCRIPTION", "HOME_URL", "BLURB", "CONFIGURE_SCRIPTS", "BUILD_SCRIPTS", "TEST_SCRIPTS", "INSTALL_SCRIPTS", "CONFIGURE_ARGS", "BUILD_ARGS", "TEST_ARGS", "INSTALL_ARGS", "CONFIGURE_ENV", "BUILD_ENV", "TEST_ENV", "INSTALL_ENV", "NEED_USERS", "NEED_GROUPS", "COLLISIONS", "DECONFLICT", "CFLAGS", "CPPFLAGS"] rule_order = ["pre-extract", "post-extract", "pre-patch", "post-patch", "pre-configure", "configure-", "post-configure", "pre-build", "build-", "post-build", "pre-install", "install-", "post-install", "pre-stow"] var_order_ = None rule_order_ = None def var_pos(self, s): if self.var_order_ is None: self.var_order_ = {} for i in range(len(self.var_order)): self.var_order_[self.var_order[i]] = i if s in self.var_order_: return self.var_order_[s] else: return -1 def rule_pos(self, s): if self.rule_order_ is None: self.rule_order_ = {} for i in range(len(self.rule_order)): self.rule_order_[self.rule_order[i]] = i i = s.find("-") if s in self.rule_order_: return self.rule_order_[s] elif i != -1 and s[:i + 1] in self.rule_order_: return self.rule_order_[s[:i + 1]] else: return -1 def load(self, fn, toplevel = True, is_include = False, top_fn = None): if fn is None: fn = gar_dir + "/gar.mk" f = StringIO.StringIO(base_rules) else: f = open(fn) parsed = parse_portfile(f) f.close() if top_fn is None: top_fn = fn seen_include = is_include var_max = -1 varadd_max = -1 rule_max = -1 for p in parsed: if p[0] == "rule": name = self.expand(p[1]) if toplevel and not seen_include: self.load_errors.append("Rule " + name + " declared before includes") pos = self.rule_pos(name) if pos != -1: if toplevel and pos < rule_max: self.load_errors.append("Rule " + name + " out of sequence") rule_max = pos if name in self.rules: self.load_errors.append("Rule " + name + " already defined") self.rules[name] = p[2] elif p[0] == "var": name = self.expand(p[1]) if toplevel and seen_include and not is_include: self.load_errors.append("Variable " + name + " declared after includes") pos = self.var_pos(name) if pos != -1: if toplevel and pos < var_max: self.load_errors.append("Variable " + name + " out of sequence") var_max = pos if name in self.vars: self.load_errors.append("Variable " + name + " already defined") self.vars[name] = p[2] self.vartypes[name] = "normal" self.varsources[name] = fn elif p[0] == "varadd": name = self.expand(p[1]) if toplevel and not seen_include: self.load_errors.append("Variable addition " + name + " declared before includes") pos = self.var_pos(name) if pos != -1: if toplevel and pos < varadd_max: self.load_errors.append("Variable addition " + name + " out of sequence") varadd_max = pos if not name in self.vars: self.vars[name] = "" self.vartypes[name] = "normal" self.varsources[name] = fn else: self.vars[name] += " " if self.vartypes[name] == "normal": self.vars[name] += p[2] else: self.vars[name] += self.expand(p[2]) elif p[0] == "varsimple": name = self.expand(p[1]) # Special case: don't complain if we're doing: # FOO := something:$(FOO) if name in self.vars and self.vartypes[name] == "simple" and p[2].find("$(" + name + ")") == -1: self.load_errors.append("Variable " + name + " already defined") self.vars[name] = self.expand(p[2]) self.vartypes[name] = "simple" self.varsources[name] = fn elif p[0] == "varmaybe": name = self.expand(p[1]) if not name in self.vars: self.vars[name] = p[2] self.vartypes[name] = "normal" self.varsources[name] = fn elif p[0] == "include": for ifn in split_whitespace(self.expand(p[1])): # Kludge: we don't want the stock rules. if ifn == "../../gar.mk": self.load(None, False, top_fn = top_fn) else: relname = os.path.join(os.path.dirname(top_fn), ifn) self.load(relname, False, top_fn = top_fn) seen_include = True elif p[0] == "export": for v in split_whitespace(self.expand(p[1])): self.exports[v] = 1 elif p[0] == "unexport": for v in split_whitespace(self.expand(p[1])): if v in self.exports: del self.exports[v] def validate(self, fn = None): if fn is None: fn = self.filename if self.load_errors != []: raise PortfileError(self.load_errors[0]) must_define = ["GARNAME", "GARVERSION", "CATEGORIES", "DESCRIPTION"] for v in must_define: if not v in self.vars: raise PortfileError("Variable " + v + " must be defined") if "INSTALL_SCRIPTS" in self.vars and not "DISTFILES" in self.vars: raise PortfileError("DISTFILES must be defined for non-stub port") if ("CONFIGURE_SCRIPTS" in self.vars or "BUILD_SCRIPTS" in self.vars) and not "INSTALL_SCRIPTS" in self.vars: raise PortfileError("INSTALL_SCRIPTS must be defined for buildable port") for stage in ["CONFIGURE", "BUILD", "INSTALL"]: if (stage + "_ARGS") in self.vars and not (stage + "_SCRIPTS") in self.vars and self.varsources[stage + "_ARGS"] != gar_dir + "/gar.mk": raise PortfileError(stage + "_ARGS specified without " + stage + "_SCRIPTS") must_not_be_empty = ["GARNAME", "GARVERSION", "DESCRIPTION"] for v in must_not_be_empty: if self.vars[v].strip() == "": raise PortfileError(v + " must not be empty") cats = self.vars["CATEGORIES"].strip().split() if len(cats) < 1: raise PortfileError("At least one category must be specified") if fn[0] != '/': fn = os.path.normpath(os.getcwd() + '/' + fn) cat_dn = os.path.dirname(os.path.dirname(fn)) if cat_dn != "": cat = os.path.basename(cat_dn) if not cat in cats: raise PortfileError("Directory name " + cat + " is not listed in categories") if "BLURB" in self.vars and self.vars["BLURB"].strip() == "FIXME": raise PortfileError("BLURB set to FIXME -- replace with HOME_URL or remove") def show(self, f = sys.stdout): vs = self.vars.keys() vs.sort() for v in vs: print >>f, v + " = " + self.variable(v) print >>f print >>f, "export " + " ".join(self.exports.keys()) print >>f rs = self.rules.keys() rs.sort() for r in rs: print >>f, r + ":" for l in self.rules[r]: print >>f, "\t" + self.expand(l) print >>f class memoise: def __init__(self, function): self.memo = {} self.function = function def __call__(self, *args): if self.memo.has_key(args): return self.memo[args] r = apply(self.function, args) self.memo[args] = r return r ws_re = re.compile("\s+") def normalise(s): global ws_re i = s.find('#') if i != -1: s = s[:i] return re.sub(ws_re, ' ', s).strip() def intersection(a, b): r = {} i = [] for x in a: r[x] = 1 for x in b: if r.has_key(x): i.append(x) return i def union(a, b): r = {} for x in a: r[x] = 1 for x in b: r[x] = 1 return r.keys() def get_package_names_x(): packages = {} categories = list_dirs_in(gar_dir) for cat in categories: if cat.startswith("gar.") or cat == "CVS": continue catdir = gar_dir + "/" + cat ps = list_dirs_in(catdir) for pkg in ps: if pkg == "CVS": continue pkgdir = catdir + "/" + pkg try: os.stat(pkgdir + "/Makefile") except OSError: continue packages[pkg] = cat + "/" + pkg return packages get_package_names = memoise(get_package_names_x) def get_config_portfile_x(): try: return Portfile(gar_dir + "/gar.conf.mk", is_include = True) except PortfileError, p: die("Error loading gar.conf.mk: ", p) get_config_portfile = memoise(get_config_portfile_x) def get_stow_dir(): return get_config_portfile().variable("prefix") def get_packages_dir(): return get_config_portfile().variable("packagesdir") def get_package_dir(package): return gar_dir + "/" + get_package_names()[package] def get_portfile_x(package): makefile_name = get_package_dir(package) + "/Makefile" try: return Portfile(makefile_name) except PortfileError, p: die("Error loading Makefile for ", package, ": ", p) get_portfile = memoise(get_portfile_x) def get_description(package): return get_portfile(package).variable("DESCRIPTION") def get_version(package): return get_portfile(package).variable("GARVERSION") def get_full_package_name(package): return get_portfile(package).variable("CATEGORIES").split()[0] + "/" + package def parse_dep_list(list): list = list.strip() if list == "": return [] else: return [s.split("/")[1] for s in split_whitespace(list)] def get_ignore_list_x(): ignore = {} for p in parse_dep_list(get_config_portfile().variable("IGNORE_DEPS")): ignore[p] = True return ignore get_ignore_list = memoise(get_ignore_list_x) def get_dependencies_x(package): ignore = get_ignore_list() deps = [] libdeps = parse_dep_list(get_portfile(package).variable("LIBDEPS")) builddeps = parse_dep_list(get_portfile(package).variable("BUILDDEPS")) allnames = get_package_names() for p in libdeps + builddeps: if not p in allnames: warn("Warning: ", package, " has dependency on non-existant package ", p, "; ignoring") continue if ignore.has_key(p): continue deps.append(p) return deps get_dependencies = memoise(get_dependencies_x) def get_categories_x(package): return split_whitespace(get_portfile(package).variable("CATEGORIES")) get_categories = memoise(get_categories_x) def get_dep_tree_x(): ps = get_package_names() tree = {} for pkg in ps: tree[pkg] = get_dependencies(pkg) return tree get_dep_tree = memoise(get_dep_tree_x) def get_req_tree_x(): dt = get_dep_tree() tree = {} for pkg in dt.keys(): tree[pkg] = [] for pkg in dt.keys(): for dep_pkg in dt[pkg]: tree[dep_pkg].append(pkg) return tree get_req_tree = memoise(get_req_tree_x) def get_requirements(package): return get_req_tree()[package] def recursive_deps_x(package, is_deps): if is_deps: tree = get_dep_tree() else: tree = get_req_tree() deps = {} def rec(deps, tree, item, stack): if item in stack: die("Recursive dependency detected: " + " -> ".join(stack + [item])) elif item in deps: return deps[item] = True for dep in tree[item]: rec(deps, tree, dep, stack + [item]) rec(deps, tree, package, []) del deps[package] return deps.keys() recursive_deps = memoise(recursive_deps_x) def get_deps_rec(package): return recursive_deps(package, True) def get_reqs_rec(package): return recursive_deps(package, False) def get_depths_x(): tree = get_dep_tree() depths = {} def rec(depths, tree, item, stack): if item in stack: die("Recursive dependency detected: " + " -> ".join(stack + [item])) elif depths.has_key(item): return depths[item] d = 0 for i in tree[item]: sd = rec(depths, tree, i, stack + [item]) if sd > d: d = sd d += 1 depths[item] = d return d for i in tree.keys(): rec(depths, tree, i, []) return depths get_depths = memoise(get_depths_x) def cmp_install_x(a, b): depths = get_depths() return cmp(depths[a], depths[b]) cmp_install = memoise(cmp_install_x) def cmp_remove(a, b): return -cmp_install(a, b) def list_dirs_in_x(dir): ds = [] for name in os.listdir(dir): mode = os.lstat(dir + "/" + name).st_mode if stat.S_ISDIR(mode): ds.append(name) return ds list_dirs_in = memoise(list_dirs_in_x) def list_packagesdir_x(): contents = {} for x in os.listdir(get_packages_dir()): name = get_packages_dir() + "/" + x contents[x] = os.lstat(name) return contents list_packagesdir = memoise(list_packagesdir_x) def get_installed_x(): installed = {} for (name, st) in list_packagesdir().items(): if stat.S_ISLNK(st.st_mode): installed[name] = os.readlink(get_packages_dir() + "/" + name) return installed get_installed = memoise(get_installed_x) def get_installed_rev_x(): rev = {} for (name, versioned) in get_installed().items(): rev[versioned] = name return rev get_installed_rev = memoise(get_installed_rev_x) def get_installed_version(package): ins = get_installed() if ins.has_key(package): return ins[package][len(package) + 1:] else: return None def get_owning_package_x(filename): pd = get_packages_dir() sd = get_stow_dir() if filename.startswith(pd): m = re.match(r'^' + pd + '/([^/]+)/.*$', filename) if m is None: die("Cannot match containing package for: " + filename) return get_installed_rev()[m.group(1)] elif filename.startswith(sd): orig_filename = filename while 1: if stat.S_ISLNK(os.lstat(filename).st_mode): filename = norm_readlink(filename) if filename.startswith(pd): break else: filename = os.path.dirname(filename) if filename == sd or filename == "/": die("Cannot find containing symlink: " + orig_filename) m = re.match(r'^' + pd + r'/([^/]+)/.*$', filename) if m is None: die("Cannot match containing package for link: " + filename) return m.group(1) else: die("Not in a package: " + filename) get_owning_package = memoise(get_owning_package_x) def run_command(cmd): pid = os.fork() if pid == 0: os.execvp(cmd[0], cmd) os._exit(20) (deadpid, status) = os.waitpid(pid, 0) return status def norm_readlink(link): assert link[0] == "/" # Resolve all symlinks in the path. outpath = "/" for part in link[1:].split("/"): outpath += part if stat.S_ISLNK(os.lstat(outpath).st_mode): outpath = os.path.normpath(os.path.join(os.path.dirname(outpath), os.readlink(outpath))) outpath += "/" return outpath[:-1] def make_operation(package, operation): print ">>> In package " + package + ": make " + operation pns = get_package_names() rc = run_command(["make", "-C", get_package_dir(package), operation]) if rc != 0: die("make failed: " + package + " " + operation) def parse_version(s): """Parse a version number into a list that can be used as a sort key.""" # This has to fudge around a bit so that -rcs sort correctly. s = s.replace("-rc", ".-") s = s.replace("rc", ".-") s = s.replace("-pre", ".-") s = s.replace("pre", ".-") s = s.replace("beta", ".-") l = s.split(".") for i in range(len(l)): try: l[i] = int(l[i]) if l[i] < 0: l[i] = -100 - l[i] except ValueError: pass l.append(0) return l # FIXME This should be a package attribute. def select_dep_type(dep): if dep == "gnome-doc-utils": return "BUILDDEPS" else: return "LIBDEPS"