Files
libretiny/builder/utils/libs-queue.py
2023-03-13 22:18:02 +01:00

216 lines
6.9 KiB
Python

# Copyright (c) Kuba Szczodrzyński 2023-02-28.
import fnmatch
from dataclasses import InitVar, dataclass, field
from glob import glob
from os.path import isdir, join
from typing import Dict, Generator, List, Tuple
from ltchiptool.util.dict import merge_dicts
from SCons.Script import DefaultEnvironment, Environment
env: Environment = DefaultEnvironment()
ENV_PUBLIC_ONLY = ["CPPPATH", "LIBPATH", "LIBS", "LINKFLAGS"]
def add_base_dir(
env: Environment,
base_dir: str,
expressions: List[str],
subst: bool = False,
):
out = []
for expr in expressions:
if expr == False:
# support '[cond] and [path]' logical expressions
continue
if expr[1] != "<" or expr[-1] != ">":
raise ValueError(f"Not a valid glob: {expr}")
if expr[2] == "$":
# do not append base path
path = expr[2:-1]
else:
path = join(base_dir, expr[2:-1])
if subst:
path = env.subst(path)
out.append(expr[0] + "<" + path + ">")
return out
def iter_expressions(expressions: List[str]) -> Generator[Tuple[str, str], None, None]:
for expr in expressions:
if expr[1:2] != "<" or expr[-1:] != ">":
yield ("+", expr)
continue
yield (expr[0], expr[2:-1])
def apply_options(env: Environment, options: Dict[str, List[str]]):
non_expr_keys = ["CPPDEFINES"]
for key, values in options.items():
if not values:
continue
if key in non_expr_keys or not isinstance(values[0], str):
env.Append(**{key: values})
continue
for dir, value in iter_expressions(values):
if dir == "+":
env.Append(**{key: [value]})
elif dir == "!":
env.Prepend(**{key: [value]})
elif dir == "-":
if value not in env[key]:
raise ValueError(f"Invalid option; {value} is not in {key}")
env[key].remove(value)
@dataclass
class Library:
env: InitVar[Environment]
name: str
base_dir: str
srcs: List[str]
includes: List[str] = field(default_factory=lambda: [])
options: Dict[str, List[str]] = field(default_factory=lambda: {})
def __post_init__(self, env: Environment):
# add base dir to all globs
self.srcs = add_base_dir(env, self.base_dir, self.srcs)
self.includes = add_base_dir(env, self.base_dir, self.includes, subst=True)
class LibraryQueue:
env: Environment
name: str
queue: List[Library]
includes: List[str]
options_public: dict
options_private: dict
prepend_includes: bool
built: bool = False
def __init__(
self,
env: Environment,
name: str,
prepend_includes: bool = False,
) -> None:
self.env = env
self.name = name
self.queue = []
self.includes = []
self.options_public = {}
self.options_private = {}
self.prepend_includes = prepend_includes
def AddLibrary(self, **kwargs):
lib = Library(env=self.env, **kwargs)
# search all include paths
for dir, expr in iter_expressions(lib.includes):
if dir == "-":
for item in fnmatch.filter(self.includes, expr):
if item in self.includes:
self.includes.remove(item)
continue
for item in glob(expr, recursive=True):
if not isdir(item):
continue
if dir == "!":
self.includes.insert(0, item)
else:
self.includes.append(item)
# move public-only options to the global env
for key in ENV_PUBLIC_ONLY:
if key not in lib.options:
continue
option = lib.options.pop(key)
self.options_public = merge_dicts(self.options_public, {key: option})
self.queue.append(lib)
def AddExternalLibrary(self, *args, **kwargs):
return self.env.AddExternalLibrary(self, *args, **kwargs)
def AppendPublic(self, **kwargs):
if "CPPPATH" in kwargs:
self.includes += kwargs["CPPPATH"]
kwargs.pop("CPPPATH")
self.options_public = merge_dicts(self.options_public, kwargs)
def AppendPrivate(self, **kwargs):
if any(key in ENV_PUBLIC_ONLY for key in kwargs.keys()):
raise ValueError("Cannot set these as private options")
self.options_private = merge_dicts(self.options_private, kwargs)
def Print(self):
def print_list(items):
print(
"\n".join(
f"{i+1: 4d}. {self.env.subst(str(item))}"
for i, item in enumerate(items)
)
)
print()
print(f"Library Queue - {self.name}")
print("Environment paths:")
print_list(self.env["CPPPATH"])
print(
"Include paths (%s):" % ("prepend" if self.prepend_includes else "append")
)
print_list(self.includes)
print("Environment options:")
opts = ["CFLAGS", "CCFLAGS", "CXXFLAGS", "CPPDEFINES", "ASFLAGS"]
opts = {k: v for k, v in self.env.items() if k in opts}
print_list(opts.items())
print("Options - public:")
print_list(self.options_public.items())
print("Options - private:")
print_list(self.options_private.items())
print("Libraries:")
print_list(self.queue)
def BuildLibraries(self):
if self.built:
raise RuntimeError("Cannot build a library queue twice")
# add public options to the environment
apply_options(self.env, self.options_public)
# treat all include paths as public
if self.prepend_includes:
self.env.Prepend(CPPPATH=self.includes)
else:
self.env.Append(CPPPATH=self.includes)
# clone the environment for the whole library queue
queue_env = self.env.Clone()
# add private options to the cloned environment
apply_options(queue_env, self.options_private)
for lib in self.queue:
if lib.options:
# clone the environment separately for each library
lib_env = queue_env.Clone()
# add library-scoped options
apply_options(lib_env, lib.options)
else:
# no library-scoped options, just use the base env
lib_env = queue_env
# build library with (name, base_dir, sources) options
target = lib_env.BuildLibrary(
join("$BUILD_DIR", lib.name), lib.base_dir, lib.srcs
)
self.env.Prepend(LIBS=[target])
self.built = True
def env_add_library_queue(
env: Environment,
name: str,
prepend_includes: bool = False,
) -> LibraryQueue:
return LibraryQueue(env, name, prepend_includes)
env.AddMethod(env_add_library_queue, "AddLibraryQueue")