This repository serve as a backup for my Maxwell-TD code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

258 lines
9.5 KiB

#!/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()