Continue work on ninja file generation.

This commit is contained in:
Sleepy Monax 2023-02-02 10:34:46 +01:00
parent e7b93db95f
commit 3dbcf7b9cc
7 changed files with 88 additions and 300 deletions

View file

@ -1,281 +0,0 @@
from copy import copy
import errno
import os
import hashlib
import signal
import requests
import subprocess
import json
import copy
import re
class Colors:
BLACK = "\033[0;30m"
RED = "\033[0;31m"
GREEN = "\033[0;32m"
BROWN = "\033[0;33m"
BLUE = "\033[0;34m"
PURPLE = "\033[0;35m"
CYAN = "\033[0;36m"
LIGHT_GRAY = "\033[0;37m"
DARK_GRAY = "\033[1;30m"
LIGHT_RED = "\033[1;31m"
LIGHT_GREEN = "\033[1;32m"
YELLOW = "\033[1;33m"
LIGHT_BLUE = "\033[1;34m"
LIGHT_PURPLE = "\033[1;35m"
LIGHT_CYAN = "\033[1;36m"
LIGHT_WHITE = "\033[1;37m"
BOLD = "\033[1m"
FAINT = "\033[2m"
ITALIC = "\033[3m"
UNDERLINE = "\033[4m"
BLINK = "\033[5m"
NEGATIVE = "\033[7m"
CROSSED = "\033[9m"
RESET = "\033[0m"
class CliException(Exception):
def __init__(self, msg: str):
self.msg = msg
def stripDups(l: list[str]) -> list[str]:
# Remove duplicates from a list
# by keeping only the last occurence
result: list[str] = []
for item in l:
if item in result:
result.remove(item)
result.append(item)
return result
def findFiles(dir: str, exts: list[str] = []) -> list[str]:
if not os.path.isdir(dir):
return []
result: list[str] = []
for f in os.listdir(dir):
if len(exts) == 0:
result.append(f)
else:
for ext in exts:
if f.endswith(ext):
result.append(os.path.join(dir, f))
break
return result
def hashFile(filename: str) -> str:
with open(filename, "rb") as f:
return hashlib.sha256(f.read()).hexdigest()
def objSha256(obj: dict, keys: list[str] = []) -> str:
toHash = {}
if len(keys) == 0:
toHash = obj
else:
for key in keys:
if key in obj:
toHash[key] = obj[key]
data = json.dumps(toHash, sort_keys=True)
return hashlib.sha256(data.encode("utf-8")).hexdigest()
def toCamelCase(s: str) -> str:
s = ''.join(x for x in s.title() if x != '_' and x != '-')
s = s[0].lower() + s[1:]
return s
def objKey(obj: dict, keys: list[str] = []) -> str:
toKey = []
if len(keys) == 0:
keys = list(obj.keys())
keys.sort()
for key in keys:
if key in obj:
if isinstance(obj[key], bool):
if obj[key]:
toKey.append(key)
else:
toKey.append(f"{toCamelCase(key)}({obj[key]})")
return "-".join(toKey)
def mkdirP(path: str) -> str:
try:
os.makedirs(path)
except OSError as exc:
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else:
raise
return path
def downloadFile(url: str) -> str:
dest = ".osdk/cache/" + hashlib.sha256(url.encode('utf-8')).hexdigest()
tmp = dest + ".tmp"
if os.path.isfile(dest):
return dest
print(f"Downloading {url} to {dest}")
try:
r = requests.get(url, stream=True)
r.raise_for_status()
mkdirP(os.path.dirname(dest))
with open(tmp, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
os.rename(tmp, dest)
return dest
except requests.exceptions.RequestException as e:
raise CliException(f"Failed to download {url}: {e}")
def runCmd(*args: str) -> bool:
try:
proc = subprocess.run(args)
except FileNotFoundError:
raise CliException(f"Failed to run {args[0]}: command not found")
except KeyboardInterrupt:
raise CliException("Interrupted")
if proc.returncode == -signal.SIGSEGV:
raise CliException("Segmentation fault")
if proc.returncode != 0:
raise CliException(
f"Failed to run {' '.join(args)}: process exited with code {proc.returncode}")
return True
def getCmdOutput(*args: str) -> str:
try:
proc = subprocess.run(args, stdout=subprocess.PIPE)
except FileNotFoundError:
raise CliException(f"Failed to run {args[0]}: command not found")
if proc.returncode == -signal.SIGSEGV:
raise CliException("Segmentation fault")
if proc.returncode != 0:
raise CliException(
f"Failed to run {' '.join(args)}: process exited with code {proc.returncode}")
return proc.stdout.decode('utf-8')
def sanitizedUname():
un = os.uname()
if un.machine == "aarch64":
un.machine = "arm64"
return un
def findLatest(command) -> str:
"""
Find the latest version of a command
Exemples
clang -> clang-15
clang++ -> clang++-15
gcc -> gcc10
"""
print("Searching for latest version of " + command)
regex = re.compile(r"^" + re.escape(command) + r"(-.[0-9]+)?$")
versions = []
for path in os.environ["PATH"].split(os.pathsep):
if os.path.isdir(path):
for f in os.listdir(path):
if regex.match(f):
versions.append(f)
if len(versions) == 0:
raise CliException(f"Failed to find {command}")
versions.sort()
chosen = versions[-1]
print(f"Using {chosen} as {command}")
return chosen
CACHE = {}
MACROS = {
"uname": lambda what: getattr(sanitizedUname(), what).lower(),
"include": lambda *path: loadJson(''.join(path)),
"join": lambda lhs, rhs: {**lhs, **rhs} if isinstance(lhs, dict) else lhs + rhs,
"concat": lambda *args: ''.join(args),
"exec": lambda *args: getCmdOutput(*args).splitlines(),
"latest": findLatest,
}
def isJexpr(jexpr: list) -> bool:
return isinstance(jexpr, list) and len(jexpr) > 0 and isinstance(jexpr[0], str) and jexpr[0].startswith("@")
def jsonEval(jexpr: list) -> any:
macro = jexpr[0][1:]
if not macro in MACROS:
raise CliException(f"Unknown macro {macro}")
return MACROS[macro](*list(map((lambda x: jsonWalk(x)), jexpr[1:])))
def jsonWalk(e: any) -> any:
if isinstance(e, dict):
for k in e:
e[jsonWalk(k)] = jsonWalk(e[k])
elif isJexpr(e):
return jsonEval(e)
elif isinstance(e, list):
for i in range(len(e)):
e[i] = jsonWalk(e[i])
return e
def loadJson(filename: str) -> dict:
try:
result = {}
if filename in CACHE:
result = CACHE[filename]
else:
with open(filename) as f:
result = jsonWalk(json.load(f))
result["dir"] = os.path.dirname(filename)
result["json"] = filename
CACHE[filename] = result
result = copy.deepcopy(result)
return result
except Exception as e:
raise CliException(f"Failed to load json {filename}: {e}")
def tryListDir(path: str) -> list[str]:
try:
return os.listdir(path)
except FileNotFoundError:
return []

View file

@ -17,6 +17,13 @@ class Args:
del self.opts[key] del self.opts[key]
return result return result
def consumeOpt(self, key: str, default: Value) -> Value:
if key in self.opts:
result = self.opts[key]
del self.opts[key]
return result
return default
def consumeArg(self) -> str | None: def consumeArg(self) -> str | None:
if len(self.args) == 0: if len(self.args) == 0:
return None return None

View file

@ -1,4 +1,4 @@
from typing import Any, TextIO from typing import TextIO
from osdk.model import ComponentManifest, TargetManifest, Props from osdk.model import ComponentManifest, TargetManifest, Props
from osdk.ninja import Writer from osdk.ninja import Writer
@ -30,7 +30,7 @@ def gen(out: TextIO, context: Context):
tool = target.tools[i] tool = target.tools[i]
rule = rules.rules[i] rule = rules.rules[i]
writer.rule( writer.rule(
i, f"{tool.cmd} {rule.rule.replace('$flags',f'${i}flags')}") i, f"{tool.cmd} {rule.rule.replace('$flags',f'${i}flags')}", deps=rule.deps)
writer.newline() writer.newline()
writer.separator("Components") writer.separator("Components")
@ -38,14 +38,14 @@ def gen(out: TextIO, context: Context):
for instance in context.instances: for instance in context.instances:
objects = instance.objsfiles() objects = instance.objsfiles()
writer.comment(f"Component: {instance.manifest.id}") writer.comment(f"Component: {instance.manifest.id}")
writer.comment(f"Resolved: {', '.join(instance.resolved)}")
writer.newline()
for obj in objects: for obj in objects:
r = rules.byFileIn(obj[0]) r = rules.byFileIn(obj[0])
if r is None: if r is None:
raise Exception(f"Unknown rule for file {obj[0]}") raise Exception(f"Unknown rule for file {obj[0]}")
writer.build(obj[1], r.id, obj[0]) t = target.tools[r.id]
writer.build(obj[1], r.id, obj[0], order_only=t.files)
writer.newline() writer.newline()
@ -53,13 +53,26 @@ def gen(out: TextIO, context: Context):
writer.build(instance.libfile(), "ar", writer.build(instance.libfile(), "ar",
list(map(lambda o: o[1], objects))) list(map(lambda o: o[1], objects)))
else: else:
libraries: list[str] = []
for req in instance.resolved:
reqInstance = context.componentByName(req)
if reqInstance is None:
raise Exception(f"Component {req} not found")
if not reqInstance.isLib():
raise Exception(f"Component {req} is not a library")
libraries.append(reqInstance.outfile())
writer.build(instance.binfile(), "ld", writer.build(instance.binfile(), "ld",
list(map(lambda o: o[1], objects))) list(map(lambda o: o[1], objects)) + libraries)
writer.newline() writer.newline()
def build(componentSpec: str, targetSpec: str = "default", props: Props = {}) -> str: def build(componentSpec: str, targetSpec: str, props: Props = {}) -> str:
context = contextFor(targetSpec, props) context = contextFor(targetSpec, props)
target = context.target target = context.target
@ -69,6 +82,26 @@ def build(componentSpec: str, targetSpec: str = "default", props: Props = {}) -
with open(ninjaPath, "w") as f: with open(ninjaPath, "w") as f:
gen(f, context) gen(f, context)
raise NotImplementedError() component = context.componentByName(componentSpec)
return "" if component is None:
raise Exception(f"Component {componentSpec} not found")
shell.exec(f"ninja", "-v", "-f", ninjaPath, component.outfile())
return component.outfile()
def buildAll(targetSpec: str, props: Props = {}) -> str:
context = contextFor(targetSpec, props)
target = context.target
shell.mkdir(target.builddir())
ninjaPath = f"{target.builddir()}/build.ninja"
with open(ninjaPath, "w") as f:
gen(f, context)
shell.exec(f"ninja", "-v", "-f", ninjaPath)
return target.builddir()

View file

@ -1,7 +1,7 @@
from typing import Callable from typing import Callable, cast
from osdk.args import Args from osdk.args import Args
from osdk import context, shell, const, vt100, model from osdk import context, shell, const, vt100, builder
Callback = Callable[[Args], None] Callback = Callable[[Args], None]
@ -30,7 +30,15 @@ def append(cmd: Cmd):
def runCmd(args: Args): def runCmd(args: Args):
pass targetSpec = cast(str, args.consumeOpt(
"target", "host-" + shell.uname().machine))
componentSpec = args.consumeArg()
if componentSpec is None:
raise Exception("Component not specified")
builder.build(componentSpec, targetSpec)
cmds += [Cmd("r", "run", "Run the target", runCmd)] cmds += [Cmd("r", "run", "Run the target", runCmd)]
@ -44,7 +52,15 @@ cmds += [Cmd("d", "debug", "Debug the target", debugCmd)]
def buildCmd(args: Args): def buildCmd(args: Args):
pass targetSpec = cast(str, args.consumeOpt(
"target", "host-" + shell.uname().machine))
componentSpec = args.consumeArg()
if componentSpec is None:
raise Exception("Component not specified")
builder.build(componentSpec, targetSpec)
cmds += [Cmd("b", "build", "Build the target", buildCmd)] cmds += [Cmd("b", "build", "Build the target", buildCmd)]

View file

@ -1,6 +1,6 @@
from typing import cast from typing import cast
from osdk.model import TargetManifest, ComponentManifest, Props from osdk.model import TargetManifest, ComponentManifest, Props, Type
from osdk.logger import Logger from osdk.logger import Logger
from osdk import const, shell, jexpr from osdk import const, shell, jexpr
@ -25,7 +25,7 @@ class ComponentInstance:
self.resolved = resolved self.resolved = resolved
def isLib(self): def isLib(self):
return self.manifest.type == "lib" return self.manifest.type == Type.LIB
def binfile(self) -> str: def binfile(self) -> str:
return f"{self.target.builddir()}/bin/{self.manifest.id}.out" return f"{self.target.builddir()}/bin/{self.manifest.id}.out"
@ -43,6 +43,11 @@ class ComponentInstance:
def libfile(self) -> str: def libfile(self) -> str:
return f"{self.target.builddir()}/lib/{self.manifest.id}.a" return f"{self.target.builddir()}/lib/{self.manifest.id}.a"
def outfile(self) -> str:
if self.isLib():
return self.libfile()
return self.binfile()
class Context: class Context:
target: TargetManifest target: TargetManifest
@ -52,6 +57,12 @@ class Context:
self.target = target self.target = target
self.instances = instances self.instances = instances
def componentByName(self, name: str) -> ComponentInstance | None:
result = list(filter(lambda x: x.manifest.id == name, self.instances))
if len(result) == 0:
return None
return result[0]
def loadAllTargets() -> list[TargetManifest]: def loadAllTargets() -> list[TargetManifest]:
files = shell.find(const.TARGETS_DIR, ["*.json"]) files = shell.find(const.TARGETS_DIR, ["*.json"])

View file

@ -49,7 +49,7 @@ class Writer(object):
break_on_hyphens=False): break_on_hyphens=False):
self.output.write('# ' + line + '\n') self.output.write('# ' + line + '\n')
def separator(self, text) -> None: def separator(self, text : str) -> None:
self.output.write(f"# --- {text} ---" + '-' * self.output.write(f"# --- {text} ---" + '-' *
(self.width - 10 - len(text)) + " #\n\n") (self.width - 10 - len(text)) + " #\n\n")

View file

@ -3,17 +3,19 @@ class Rule:
fileIn: list[str] fileIn: list[str]
fileOut: list[str] fileOut: list[str]
rule: str rule: str
deps: str | None = None
def __init__(self, id: str, fileIn: list[str], fileOut: list[str], rule: str): def __init__(self, id: str, fileIn: list[str], fileOut: list[str], rule: str, deps: str | None = None):
self.id = id self.id = id
self.fileIn = fileIn self.fileIn = fileIn
self.fileOut = fileOut self.fileOut = fileOut
self.rule = rule self.rule = rule
self.deps = deps
rules: dict[str, Rule] = { rules: dict[str, Rule] = {
"cc": Rule("cc", ["c"], ["o"], "-c -o $out $in -MD -MF $out.d $flags"), "cc": Rule("cc", ["c"], ["o"], "-c -o $out $in -MD -MF $out.d $flags", "$out.d"),
"cxx": Rule("cxx", ["cpp", "cc", "cxx"], ["o"], "-c -o $out $in -MD -MF $out.d $flags"), "cxx": Rule("cxx", ["cpp", "cc", "cxx"], ["o"], "-c -o $out $in -MD -MF $out.d $flags", "$out.d"),
"as": Rule("as", ["s", "asm", "S"], ["o"], "-o $out $in $flags"), "as": Rule("as", ["s", "asm", "S"], ["o"], "-o $out $in $flags"),
"ar": Rule("ar", ["o"], ["a"], "$flags $out $in"), "ar": Rule("ar", ["o"], ["a"], "$flags $out $in"),
"ld": Rule("ld", ["o", "a"], ["out"], "$flags $out $in"), "ld": Rule("ld", ["o", "a"], ["out"], "$flags $out $in"),