#!/usr/bin/env python3 from __future__ import annotations import argparse, subprocess, shutil, os from enum import Enum from pathlib import Path LIB_DIR = Path("libraries/clone") INSTALL_DIR = Path("libraries") SYSTEM_INSTALL_DIR = Path("/usr/local") Compatibility = Enum('Compatibility', ['Equal', 'AtLeast', 'AtMost']) CompatSymbol = {Compatibility.Equal: '==', Compatibility.AtLeast: ">=", Compatibility.AtMost: "<="} SymbolCompat = {'==': Compatibility.Equal, ">=": Compatibility.AtLeast, "<=": Compatibility.AtMost} def make_box(string: str): length = len(string) - 3 print("╔" + "="*(length-2) + "╗") print(f"┇ {string} ┇") print("╚" + "="*(length-2) + "╝ ") def error(predicate: bool, error: str): if predicate: print(f'[\033[1;31mERROR\033[0m] {error}') quit() class Library: def __init__(self, name: str, version: str, compatibility: Compatibility): self.name = name self.version = [int(x) for x in version.split(".")] self.compatibility = compatibility self.id = name if version == "0.0.0" else f'{name}-{version}' def exists(self): return (INSTALL_DIR / self.id).is_dir() def is_satisfied_by(self, dep: Library): if self.compatibility == Compatibility.Equal: return self.version == dep.version elif self.compatibility == Compatibility.AtLeast: return self.version <= dep.version elif self.compatibility == Compatibility.AtMost: return self.version >= dep.version @staticmethod def from_name(name: str) -> Library: ind = name.rfind("-") if ind == -1: return Library(name, "0.0.0", Compatibility.Equal) else: error(not name[ind+1].isnumeric(), f"The library '{name}' has an invalid name.") return Library(name[:ind], name[ind+1:], Compatibility.Equal) @staticmethod def from_line(line: str) -> Library: line = line.split(" ") if len(line) == 1: return Library(line[0], "0.0.0", Compatibility.AtLeast) try: return Library(line[0], line[2], SymbolCompat[line[1]]) except (KeyError, IndexError): error(True, f"The dependency line '{line}' is formatted improperly.") class Node: def __init__(self, lib: Library): self.lib = lib self.parent = None self.dependencies = [] self.visited = False def build(self, prefix: Path, verbose: bool) -> int: head = " ── " space = " " ok = " [\033[1;32mOK\033[0m] " err = " [\033[1;31mERR\033[0m] " lib_prefix = prefix / self.lib.id setup = LIB_DIR / self.lib.id / "SETUP" error(not setup.is_file(), f"The library '{self.lib.id}' does not have a SETUP file.") make_box(f"Building '\033[1;96m{self.lib.id}\033[0m'") if lib_prefix.is_dir(): print(f"{ok}Library is already built, installation at '{lib_prefix}'\n") return 0 deps = " ".join([str(prefix / dep) for dep in self.dependencies]) commands = f"{setup} {lib_prefix} {deps}".split(" ") if verbose: p = subprocess.Popen(commands, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) else: p = subprocess.Popen(commands, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True) rows, _ = shutil.get_terminal_size((80, 20)) s = rows - 10 - len(head) for line in (p.stdout if verbose else p.stderr): line = "\n".join([(head if i == 0 else space) + line[i: i + s] for i in range(0, len(line), s)]) print(line, end="") returncode = p.poll() if returncode == 0: print(f"{ok}Finished building library, installation at '{lib_prefix}'\n") return 0 else: print(f"{err}Error while building library. Return code: {returncode}\n") return 1 class DependencyTree: def __init__(self): self.all_deps = dict() self.roots = [] def add_library(self, lib_dir: Path): lib = Library.from_name(lib_dir.name) if lib.id not in self.all_deps: self.all_deps[lib.id] = Node(lib) else: error(True, "Duplicate libaries, shouldn't be possible.") dep_file = lib_dir / "DEPENDENCIES" error(not dep_file.is_file(), f"The library '{lib.id}' does not have a DEPENDENCIES file.") lib_deps = open(dep_file, "r").read().splitlines() for dep_line in lib_deps: lib_dep = Library.from_line(dep_line) self.all_deps[lib.id].dependencies.append(lib_dep) def build(self): # Validate dependencies and build tree. for node in self.all_deps.values(): satisfied_deps = [] for dep in node.dependencies: fails = "" satisfied = False for node_check in self.all_deps.values(): if node_check.lib.name != dep.name: continue if dep.is_satisfied_by(node_check.lib): satisfied = True satisfied_deps.append(node_check.lib.id) node_check.parent = node.lib.id break else: fails += f', {node_check.lib.id}' if not satisfied: vers = ".".join([str(x) for x in dep.version]) dep_line = f'{dep.name} {CompatSymbol[dep.compatibility]} {vers}' error(True, f"Dependency '{dep_line}' from '{node.lib.id}' is not satisfied "\ f"by any of the following:{fails[1:]}") self.all_deps[node.lib.id].dependencies = satisfied_deps # Get roots of the tree. for node in self.all_deps.values(): if node.parent is None: self.roots.append(node.lib.id) def __repr__(self): return self.string_helper(0, self.roots)[:-1] def string_helper(self, layer, deps): line = "" for i, node_id in enumerate(deps): node = self.all_deps[node_id] line += "│ "*layer + f"├─{node.lib.id}\n" line += self.string_helper(layer+1, node.dependencies) return line def reverse_traversal(self) -> List[Node]: rt = self.reverse_helper(self.roots)[::-1] seen = set() return [x for x in rt if not (x in seen or seen.add(x))] def reverse_helper(self, libs): traversal = [] for node_id in libs: node = self.all_deps[node_id] traversal.append(node_id) traversal += self.reverse_helper(node.dependencies) return traversal def main(): parser = argparse.ArgumentParser(description="Sets up the libraries necessary in " \ "the local 'libraries/' environment, unless otherwise specified. " \ "Searches the 'libraries/clone' to install libaries.") parser.add_argument("--verbose", help="Output all setup information.", action="store_true") parser.add_argument("--tree", help="Visualize the dependency tree.", action="store_true") parser.add_argument("--system", help="Install packages to the system '/usr/local' folder.", action="store_true") args = parser.parse_args() tree = DependencyTree() for lib_dir in LIB_DIR.iterdir(): tree.add_library(lib_dir) tree.build() # Build libraries lib_build_order = tree.reverse_traversal() if args.tree: print(lib_build_order) quit() failed = 0 for lib_id in lib_build_order: prefix = SYSTEM_INSTALL_DIR if args.system else INSTALL_DIR failed += tree.all_deps[lib_id].build(prefix.resolve(), args.verbose) total = len(lib_build_order) print("Building process finished:") if not failed: print(f" [\033[1;32mPASS\033[0m] ({total - failed}/{total})") inc_dir, lib_dir = INSTALL_DIR / "include", INSTALL_DIR / "lib" inc_dir.mkdir(exist_ok=True) lib_dir.mkdir(exist_ok=True) for sym in inc_dir.iterdir(): sym.unlink() for sym in lib_dir.iterdir(): if not sym.is_dir(): sym.unlink() lib_names = dict() # name, id for lib_id in lib_build_order: name = tree.all_deps[lib_id].lib.name if name in lib_names: # Name already exists, so include version in name. if lib_names[name] != 0: # Name hasn't been marked as duplicate. lib_names[lib_names[name]] = lib_names[name] # Make old id as a new pair. lib_names[name] = 0 # Mark the old name as 0. lib_names[lib_id] = lib_id # Add the new one as an id pair. else: # Make (name, id) pair. lib_names[name] = lib_id # Apply include symlinks. for lib_name, lib_id in lib_names.items(): (inc_dir / lib_name).symlink_to(INSTALL_DIR.resolve() / lib_id / "include", target_is_directory=True) # Apply lib symlinks. for lib_id in lib_build_order: library_lib_dir = INSTALL_DIR / lib_id / "lib" if not library_lib_dir.is_dir(): continue for f in library_lib_dir.iterdir(): if f.suffix == ".a": (lib_dir / f.name).symlink_to(f.resolve()) print("Applied symlinks in 'libraries/include' and 'libraries/lib'") else: print(f" [\033[1;31mFAIL\033[0m] ({total - failed}/{total})") if __name__ == "__main__": main() main()