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
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()
|
|
|