Rewritten using typed python and simplified the model.
This commit is contained in:
parent
0dd7653de2
commit
a3ae84fde9
23 changed files with 1478 additions and 486 deletions
216
osdk-old/__init__.py
Normal file
216
osdk-old/__init__.py
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
import importlib
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
import osdk.builder as builder
|
||||||
|
import osdk.utils as utils
|
||||||
|
import osdk.targets as targets
|
||||||
|
import osdk.manifests as manifests
|
||||||
|
|
||||||
|
__version__ = "0.3.2"
|
||||||
|
|
||||||
|
CMDS = {}
|
||||||
|
|
||||||
|
|
||||||
|
def parseOptions(args: list[str]) -> dict:
|
||||||
|
result = {
|
||||||
|
'opts': {},
|
||||||
|
'args': []
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in args:
|
||||||
|
if arg.startswith("--"):
|
||||||
|
if "=" in arg:
|
||||||
|
key, value = arg[2:].split("=", 1)
|
||||||
|
result['opts'][key] = value
|
||||||
|
else:
|
||||||
|
result['opts'][arg[2:]] = True
|
||||||
|
else:
|
||||||
|
result['args'].append(arg)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def propsFromOptions(opt: dict) -> dict:
|
||||||
|
result = {}
|
||||||
|
for key in opt:
|
||||||
|
if key.startswith("prop:"):
|
||||||
|
result[key[5:]] = opt[key]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def runCmd(opts: dict, args: list[str]) -> None:
|
||||||
|
props = propsFromOptions(opts)
|
||||||
|
|
||||||
|
if len(args) == 0:
|
||||||
|
print(f"Usage: osdk run <component>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
out = builder.buildOne(opts.get('target', 'default'), args[0], props)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"{utils.Colors.BOLD}Running: {args[0]}{utils.Colors.RESET}")
|
||||||
|
utils.runCmd(out, *args[1:])
|
||||||
|
print()
|
||||||
|
print(f"{utils.Colors.GREEN}Process exited with success{utils.Colors.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def debugCmd(opts: dict, args: list[str]) -> None:
|
||||||
|
props = propsFromOptions(opts)
|
||||||
|
if len(args) == 0:
|
||||||
|
print(f"Usage: osdk debug <component>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
out = builder.buildOne(opts.get('target', 'default:debug'), args[0], props)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"{utils.Colors.BOLD}Debugging: {args[0]}{utils.Colors.RESET}")
|
||||||
|
utils.runCmd("/usr/bin/lldb", "-o", "run", out, *args[1:])
|
||||||
|
print()
|
||||||
|
print(f"{utils.Colors.GREEN}Process exited with success{utils.Colors.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def buildCmd(opts: dict, args: list[str]) -> None:
|
||||||
|
props = propsFromOptions(opts)
|
||||||
|
allTargets = opts.get('all-targets', False)
|
||||||
|
targetName = opts.get('target', 'default')
|
||||||
|
|
||||||
|
if allTargets:
|
||||||
|
for target in targets.available():
|
||||||
|
if len(args) == 0:
|
||||||
|
builder.buildAll(target, props)
|
||||||
|
else:
|
||||||
|
for component in args:
|
||||||
|
builder.buildOne(target, component, props)
|
||||||
|
else:
|
||||||
|
if len(args) == 0:
|
||||||
|
builder.buildAll(targetName, props)
|
||||||
|
else:
|
||||||
|
for component in args:
|
||||||
|
builder.buildOne(targetName, component, props)
|
||||||
|
|
||||||
|
|
||||||
|
def listCmd(opts: dict, args: list[str]) -> None:
|
||||||
|
props = propsFromOptions(opts)
|
||||||
|
targetName = opts.get('target', 'default')
|
||||||
|
target = targets.load(targetName, props)
|
||||||
|
components = manifests.loadAll(["src"], target)
|
||||||
|
|
||||||
|
print(f"Available components for target '{targetName}':")
|
||||||
|
componentsNames = list(components.keys())
|
||||||
|
componentsNames.sort()
|
||||||
|
for component in componentsNames:
|
||||||
|
if components[component]["enabled"]:
|
||||||
|
print(" " + component)
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
def cleanCmd(opts: dict, args: list[str]) -> None:
|
||||||
|
shutil.rmtree(".osdk/build", ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def nukeCmd(opts: dict, args: list[str]) -> None:
|
||||||
|
shutil.rmtree(".osdk", ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def helpCmd(opts: dict, args: list[str]) -> None:
|
||||||
|
print(f"Usage: osdk <command> [options...] [<args...>]")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
print("Description:")
|
||||||
|
print(" Operating System Development Kit.")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
print("Commands:")
|
||||||
|
for cmd in CMDS:
|
||||||
|
print(" " + cmd + " - " + CMDS[cmd]["desc"])
|
||||||
|
print("")
|
||||||
|
|
||||||
|
print("Targets:")
|
||||||
|
availableTargets = targets.available()
|
||||||
|
if len(availableTargets) == 0:
|
||||||
|
print(" No targets available")
|
||||||
|
else:
|
||||||
|
for targetName in targets.available():
|
||||||
|
print(" " + targetName)
|
||||||
|
print("")
|
||||||
|
|
||||||
|
print("Variants:")
|
||||||
|
for var in targets.VARIANTS:
|
||||||
|
print(" " + var)
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
def versionCmd(opts: dict, args: list[str]) -> None:
|
||||||
|
print("OSDK v" + __version__)
|
||||||
|
|
||||||
|
|
||||||
|
CMDS = {
|
||||||
|
"run": {
|
||||||
|
"func": runCmd,
|
||||||
|
"desc": "Run a component on the host",
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"func": debugCmd,
|
||||||
|
"desc": "Run a component on the host in debug mode",
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"func": buildCmd,
|
||||||
|
"desc": "Build one or more components",
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"func": listCmd,
|
||||||
|
"desc": "List available components",
|
||||||
|
},
|
||||||
|
"clean": {
|
||||||
|
"func": cleanCmd,
|
||||||
|
"desc": "Clean the build directory",
|
||||||
|
},
|
||||||
|
"nuke": {
|
||||||
|
"func": nukeCmd,
|
||||||
|
"desc": "Clean the build directory and cache",
|
||||||
|
},
|
||||||
|
"help": {
|
||||||
|
"func": helpCmd,
|
||||||
|
"desc": "Show this help message",
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"func": versionCmd,
|
||||||
|
"desc": "Show current version",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def loadPlugin(path: str) -> ModuleType:
|
||||||
|
"""Load a plugin from a path"""
|
||||||
|
spec = importlib.util.spec_from_file_location("plugin", path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
for files in utils.tryListDir("meta/plugins"):
|
||||||
|
if files.endswith(".py"):
|
||||||
|
plugin = loadPlugin(f"meta/plugins/{files}")
|
||||||
|
CMDS[plugin.__plugin__["name"]] = plugin.__plugin__
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argv = sys.argv
|
||||||
|
try:
|
||||||
|
if len(argv) < 2:
|
||||||
|
helpCmd({}, [])
|
||||||
|
else:
|
||||||
|
o = parseOptions(argv[2:])
|
||||||
|
if not argv[1] in CMDS:
|
||||||
|
print(f"Unknown command: {argv[1]}")
|
||||||
|
print("")
|
||||||
|
print(f"Use '{argv[0]} help' for a list of commands")
|
||||||
|
return 1
|
||||||
|
CMDS[argv[1]]["func"](o['opts'], o['args'])
|
||||||
|
return 0
|
||||||
|
except utils.CliException as e:
|
||||||
|
print()
|
||||||
|
print(f"{utils.Colors.RED}{e.msg}{utils.Colors.RESET}")
|
||||||
|
return 1
|
5
osdk-old/__main__.py
Executable file
5
osdk-old/__main__.py
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import main
|
||||||
|
|
||||||
|
sys.exit(main())
|
|
@ -86,6 +86,7 @@ def resolveDeps(manifests: dict) -> dict:
|
||||||
keep, dep, res = resolve(dep, stack)
|
keep, dep, res = resolve(dep, stack)
|
||||||
if not keep:
|
if not keep:
|
||||||
stack.pop()
|
stack.pop()
|
||||||
|
print(f"Disabling {key} because we are missing a deps")
|
||||||
return False, "", []
|
return False, "", []
|
||||||
result.append(dep)
|
result.append(dep)
|
||||||
result += res
|
result += res
|
281
osdk-old/utils.py
Normal file
281
osdk-old/utils.py
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
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 []
|
225
osdk/__init__.py
225
osdk/__init__.py
|
@ -1,216 +1,23 @@
|
||||||
import importlib
|
|
||||||
import shutil
|
|
||||||
import sys
|
import sys
|
||||||
from types import ModuleType
|
|
||||||
|
|
||||||
import osdk.build as build
|
|
||||||
import osdk.utils as utils
|
|
||||||
import osdk.targets as targets
|
|
||||||
import osdk.manifests as manifests
|
|
||||||
|
|
||||||
__version__ = "0.3.2"
|
|
||||||
|
|
||||||
CMDS = {}
|
|
||||||
|
|
||||||
|
|
||||||
def parseOptions(args: list[str]) -> dict:
|
from osdk.args import parse
|
||||||
result = {
|
from osdk.cmds import exec, usage
|
||||||
'opts': {},
|
from osdk.plugins import loadAll
|
||||||
'args': []
|
import osdk.vt100 as vt100
|
||||||
}
|
|
||||||
|
|
||||||
for arg in args:
|
|
||||||
if arg.startswith("--"):
|
|
||||||
if "=" in arg:
|
|
||||||
key, value = arg[2:].split("=", 1)
|
|
||||||
result['opts'][key] = value
|
|
||||||
else:
|
|
||||||
result['opts'][arg[2:]] = True
|
|
||||||
else:
|
|
||||||
result['args'].append(arg)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def propsFromOptions(opt: dict) -> dict:
|
def main() -> int:
|
||||||
result = {}
|
|
||||||
for key in opt:
|
|
||||||
if key.startswith("prop:"):
|
|
||||||
result[key[5:]] = opt[key]
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def runCmd(opts: dict, args: list[str]) -> None:
|
|
||||||
props = propsFromOptions(opts)
|
|
||||||
|
|
||||||
if len(args) == 0:
|
|
||||||
print(f"Usage: osdk run <component>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
out = build.buildOne(opts.get('target', 'default'), args[0], props)
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(f"{utils.Colors.BOLD}Running: {args[0]}{utils.Colors.RESET}")
|
|
||||||
utils.runCmd(out, *args[1:])
|
|
||||||
print()
|
|
||||||
print(f"{utils.Colors.GREEN}Process exited with success{utils.Colors.RESET}")
|
|
||||||
|
|
||||||
|
|
||||||
def debugCmd(opts: dict, args: list[str]) -> None:
|
|
||||||
props = propsFromOptions(opts)
|
|
||||||
if len(args) == 0:
|
|
||||||
print(f"Usage: osdk debug <component>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
out = build.buildOne(opts.get('target', 'default:debug'), args[0], props)
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(f"{utils.Colors.BOLD}Debugging: {args[0]}{utils.Colors.RESET}")
|
|
||||||
utils.runCmd("/usr/bin/lldb", "-o", "run", out, *args[1:])
|
|
||||||
print()
|
|
||||||
print(f"{utils.Colors.GREEN}Process exited with success{utils.Colors.RESET}")
|
|
||||||
|
|
||||||
|
|
||||||
def buildCmd(opts: dict, args: list[str]) -> None:
|
|
||||||
props = propsFromOptions(opts)
|
|
||||||
allTargets = opts.get('all-targets', False)
|
|
||||||
targetName = opts.get('target', 'default')
|
|
||||||
|
|
||||||
if allTargets:
|
|
||||||
for target in targets.available():
|
|
||||||
if len(args) == 0:
|
|
||||||
build.buildAll(target, props)
|
|
||||||
else:
|
|
||||||
for component in args:
|
|
||||||
build.buildOne(target, component, props)
|
|
||||||
else:
|
|
||||||
if len(args) == 0:
|
|
||||||
build.buildAll(targetName, props)
|
|
||||||
else:
|
|
||||||
for component in args:
|
|
||||||
build.buildOne(targetName, component, props)
|
|
||||||
|
|
||||||
|
|
||||||
def listCmd(opts: dict, args: list[str]) -> None:
|
|
||||||
props = propsFromOptions(opts)
|
|
||||||
targetName = opts.get('target', 'default')
|
|
||||||
target = targets.load(targetName, props)
|
|
||||||
components = manifests.loadAll(["src"], target)
|
|
||||||
|
|
||||||
print(f"Available components for target '{targetName}':")
|
|
||||||
componentsNames = list(components.keys())
|
|
||||||
componentsNames.sort()
|
|
||||||
for component in componentsNames:
|
|
||||||
if components[component]["enabled"]:
|
|
||||||
print(" " + component)
|
|
||||||
print("")
|
|
||||||
|
|
||||||
|
|
||||||
def cleanCmd(opts: dict, args: list[str]) -> None:
|
|
||||||
shutil.rmtree(".osdk/build", ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
def nukeCmd(opts: dict, args: list[str]) -> None:
|
|
||||||
shutil.rmtree(".osdk", ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
def helpCmd(opts: dict, args: list[str]) -> None:
|
|
||||||
print(f"Usage: osdk <command> [options...] [<args...>]")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
print("Description:")
|
|
||||||
print(" Operating System Development Kit.")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
print("Commands:")
|
|
||||||
for cmd in CMDS:
|
|
||||||
print(" " + cmd + " - " + CMDS[cmd]["desc"])
|
|
||||||
print("")
|
|
||||||
|
|
||||||
print("Targets:")
|
|
||||||
availableTargets = targets.available()
|
|
||||||
if len(availableTargets) == 0:
|
|
||||||
print(" No targets available")
|
|
||||||
else:
|
|
||||||
for targetName in targets.available():
|
|
||||||
print(" " + targetName)
|
|
||||||
print("")
|
|
||||||
|
|
||||||
print("Variants:")
|
|
||||||
for var in targets.VARIANTS:
|
|
||||||
print(" " + var)
|
|
||||||
print("")
|
|
||||||
|
|
||||||
|
|
||||||
def versionCmd(opts: dict, args: list[str]) -> None:
|
|
||||||
print("OSDK v" + __version__)
|
|
||||||
|
|
||||||
|
|
||||||
CMDS = {
|
|
||||||
"run": {
|
|
||||||
"func": runCmd,
|
|
||||||
"desc": "Run a component on the host",
|
|
||||||
},
|
|
||||||
"debug": {
|
|
||||||
"func": debugCmd,
|
|
||||||
"desc": "Run a component on the host in debug mode",
|
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"func": buildCmd,
|
|
||||||
"desc": "Build one or more components",
|
|
||||||
},
|
|
||||||
"list": {
|
|
||||||
"func": listCmd,
|
|
||||||
"desc": "List available components",
|
|
||||||
},
|
|
||||||
"clean": {
|
|
||||||
"func": cleanCmd,
|
|
||||||
"desc": "Clean the build directory",
|
|
||||||
},
|
|
||||||
"nuke": {
|
|
||||||
"func": nukeCmd,
|
|
||||||
"desc": "Clean the build directory and cache",
|
|
||||||
},
|
|
||||||
"help": {
|
|
||||||
"func": helpCmd,
|
|
||||||
"desc": "Show this help message",
|
|
||||||
},
|
|
||||||
"version": {
|
|
||||||
"func": versionCmd,
|
|
||||||
"desc": "Show current version",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def loadPlugin(path: str) -> ModuleType:
|
|
||||||
"""Load a plugin from a path"""
|
|
||||||
spec = importlib.util.spec_from_file_location("plugin", path)
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
for files in utils.tryListDir("meta/plugins"):
|
|
||||||
if files.endswith(".py"):
|
|
||||||
plugin = loadPlugin(f"meta/plugins/{files}")
|
|
||||||
CMDS[plugin.__plugin__["name"]] = plugin.__plugin__
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
argv = sys.argv
|
|
||||||
try:
|
try:
|
||||||
if len(argv) < 2:
|
loadAll()
|
||||||
helpCmd({}, [])
|
a = parse(sys.argv[1:])
|
||||||
else:
|
exec(a)
|
||||||
o = parseOptions(argv[2:])
|
return 0
|
||||||
if not argv[1] in CMDS:
|
except Exception as e:
|
||||||
print(f"Unknown command: {argv[1]}")
|
print(f"{vt100.RED}{e}{vt100.RESET}")
|
||||||
print("")
|
|
||||||
print(f"Use '{argv[0]} help' for a list of commands")
|
|
||||||
return 1
|
|
||||||
CMDS[argv[1]]["func"](o['opts'], o['args'])
|
|
||||||
return 0
|
|
||||||
except utils.CliException as e:
|
|
||||||
print()
|
print()
|
||||||
print(f"{utils.Colors.RED}{e.msg}{utils.Colors.RESET}")
|
|
||||||
return 1
|
usage()
|
||||||
|
print()
|
||||||
|
|
||||||
|
raise e
|
||||||
|
|
0
osdk/__main__.py
Executable file → Normal file
0
osdk/__main__.py
Executable file → Normal file
42
osdk/args.py
Normal file
42
osdk/args.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
Value = str | bool | int
|
||||||
|
|
||||||
|
|
||||||
|
class Args:
|
||||||
|
opts: dict[str, Value]
|
||||||
|
args: list[str]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.opts = {}
|
||||||
|
self.args = []
|
||||||
|
|
||||||
|
def consumePrefix(self, prefix: str) -> dict[str, Value]:
|
||||||
|
result: dict[str, Value] = {}
|
||||||
|
for key, value in self.opts.items():
|
||||||
|
if key.startswith(prefix):
|
||||||
|
result[key[len(prefix):]] = value
|
||||||
|
del self.opts[key]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def consumeArg(self) -> str | None:
|
||||||
|
if len(self.args) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
first = self.args[0]
|
||||||
|
del self.args[0]
|
||||||
|
return first
|
||||||
|
|
||||||
|
|
||||||
|
def parse(args: list[str]) -> Args:
|
||||||
|
result = Args()
|
||||||
|
|
||||||
|
for arg in args:
|
||||||
|
if arg.startswith("--"):
|
||||||
|
if "=" in arg:
|
||||||
|
key, value = arg[2:].split("=", 1)
|
||||||
|
result.opts[key] = value
|
||||||
|
else:
|
||||||
|
result.opts[arg[2:]] = True
|
||||||
|
else:
|
||||||
|
result.args.append(arg)
|
||||||
|
|
||||||
|
return result
|
43
osdk/builder.py
Normal file
43
osdk/builder.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
from typing import Any, TextIO
|
||||||
|
|
||||||
|
from osdk.model import ComponentManifest, TargetManifest, Props
|
||||||
|
from osdk.ninja import Writer
|
||||||
|
from osdk.logger import Logger
|
||||||
|
from osdk.context import Context, contextFor
|
||||||
|
from osdk import shell
|
||||||
|
|
||||||
|
logger = Logger("builder")
|
||||||
|
|
||||||
|
|
||||||
|
def gen(out: TextIO, context: Context):
|
||||||
|
writer = Writer(out)
|
||||||
|
|
||||||
|
target = context.target
|
||||||
|
|
||||||
|
writer.comment("File generated by the build system, do not edit")
|
||||||
|
writer.newline()
|
||||||
|
|
||||||
|
writer.separator("Tools")
|
||||||
|
for key in target.tools:
|
||||||
|
tool = target.tools[key]
|
||||||
|
writer.variable(key, tool.cmd)
|
||||||
|
writer.variable(
|
||||||
|
key + "flags", " ".join(tool.args))
|
||||||
|
writer.newline()
|
||||||
|
|
||||||
|
writer.separator("Rules")
|
||||||
|
|
||||||
|
|
||||||
|
def build(componentSpec: str, targetSpec: str = "default", 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)
|
||||||
|
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
return ""
|
138
osdk/cmds.py
Normal file
138
osdk/cmds.py
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from osdk.args import Args
|
||||||
|
from osdk import context, shell, const, vt100, model
|
||||||
|
|
||||||
|
Callback = Callable[[Args], None]
|
||||||
|
|
||||||
|
|
||||||
|
class Cmd:
|
||||||
|
shortName: str
|
||||||
|
longName: str
|
||||||
|
helpText: str
|
||||||
|
callback: Callable[[Args], None]
|
||||||
|
isPlugin: bool = False
|
||||||
|
|
||||||
|
def __init__(self, shortName: str, longName: str, helpText: str, callback: Callable[[Args], None]):
|
||||||
|
self.shortName = shortName
|
||||||
|
self.longName = longName
|
||||||
|
self.helpText = helpText
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
|
||||||
|
cmds: list[Cmd] = []
|
||||||
|
|
||||||
|
|
||||||
|
def append(cmd: Cmd):
|
||||||
|
cmd.isPlugin = True
|
||||||
|
cmds.append(cmd)
|
||||||
|
cmds.sort(key=lambda c: c.shortName)
|
||||||
|
|
||||||
|
|
||||||
|
def runCmd(args: Args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
cmds += [Cmd("r", "run", "Run the target", runCmd)]
|
||||||
|
|
||||||
|
|
||||||
|
def debugCmd(args: Args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
cmds += [Cmd("d", "debug", "Debug the target", debugCmd)]
|
||||||
|
|
||||||
|
|
||||||
|
def buildCmd(args: Args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
cmds += [Cmd("b", "build", "Build the target", buildCmd)]
|
||||||
|
|
||||||
|
|
||||||
|
def listCmd(args: Args):
|
||||||
|
components = context.loadAllComponents()
|
||||||
|
targets = context.loadAllTargets()
|
||||||
|
|
||||||
|
vt100.title("Components")
|
||||||
|
if len(components) == 0:
|
||||||
|
print(f" (No components available)")
|
||||||
|
else:
|
||||||
|
print(vt100.indent(vt100.wordwrap(
|
||||||
|
", ".join(map(lambda m: m.id, components)))))
|
||||||
|
print()
|
||||||
|
|
||||||
|
vt100.title("Targets")
|
||||||
|
|
||||||
|
if len(targets) == 0:
|
||||||
|
print(f" (No targets available)")
|
||||||
|
else:
|
||||||
|
print(vt100.indent(vt100.wordwrap(", ".join(map(lambda m: m.id, targets)))))
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
cmds += [Cmd("l", "list", "List the targets", listCmd)]
|
||||||
|
|
||||||
|
|
||||||
|
def cleanCmd(args: Args):
|
||||||
|
shell.rmrf(const.BUILD_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
cmds += [Cmd("c", "clean", "Clean the build directory", cleanCmd)]
|
||||||
|
|
||||||
|
|
||||||
|
def nukeCmd(args: Args):
|
||||||
|
shell.rmrf(const.OSDK_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
cmds += [Cmd("n", "nuke", "Clean the build directory and cache", nukeCmd)]
|
||||||
|
|
||||||
|
|
||||||
|
def helpCmd(args: Args):
|
||||||
|
usage()
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
vt100.title("Description")
|
||||||
|
print(" Operating System Development Kit.")
|
||||||
|
|
||||||
|
print()
|
||||||
|
vt100.title("Commands")
|
||||||
|
for cmd in cmds:
|
||||||
|
pluginText = ""
|
||||||
|
if cmd.isPlugin:
|
||||||
|
pluginText = f"{vt100.CYAN}(plugin){vt100.RESET}"
|
||||||
|
|
||||||
|
print(
|
||||||
|
f" {vt100.GREEN}{cmd.shortName}{vt100.RESET} {cmd.longName} - {cmd.helpText} {pluginText}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
cmds += [Cmd("h", "help", "Show this help message", helpCmd)]
|
||||||
|
|
||||||
|
|
||||||
|
def versionCmd(args: Args):
|
||||||
|
print(f"OSDK v{const.VERSION}\n")
|
||||||
|
|
||||||
|
|
||||||
|
cmds += [Cmd("v", "version", "Show current version", versionCmd)]
|
||||||
|
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
print(f"Usage: {const.ARGV0} <command> [args...]")
|
||||||
|
|
||||||
|
|
||||||
|
def exec(args: Args):
|
||||||
|
cmd = args.consumeArg()
|
||||||
|
|
||||||
|
if cmd is None:
|
||||||
|
raise Exception("No command specified")
|
||||||
|
|
||||||
|
for c in cmds:
|
||||||
|
if c.shortName == cmd or c.longName == cmd:
|
||||||
|
c.callback(args)
|
||||||
|
return
|
||||||
|
|
||||||
|
raise Exception(f"Unknown command {cmd}")
|
12
osdk/const.py
Normal file
12
osdk/const.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
VERSION = "0.4.0"
|
||||||
|
MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
ARGV0 = os.path.basename(sys.argv[0])
|
||||||
|
OSDK_DIR = ".osdk"
|
||||||
|
BUILD_DIR = f"{OSDK_DIR}/build"
|
||||||
|
CACHE_DIR = f"{OSDK_DIR}/cache"
|
||||||
|
SRC_DIR = "src/"
|
||||||
|
META_DIR = f"meta"
|
||||||
|
TARGETS_DIR = f"{META_DIR}/targets"
|
149
osdk/context.py
Normal file
149
osdk/context.py
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
from osdk.model import TargetManifest, ComponentManifest, Props
|
||||||
|
from osdk.logger import Logger
|
||||||
|
from osdk import const, shell, jexpr
|
||||||
|
|
||||||
|
logger = Logger("context")
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentInstance:
|
||||||
|
target: TargetManifest
|
||||||
|
manifest: ComponentManifest
|
||||||
|
sources: list[str] = []
|
||||||
|
resolved: list[str] = []
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
target: TargetManifest,
|
||||||
|
manifest: ComponentManifest,
|
||||||
|
sources: list[str],
|
||||||
|
resolved: list[str]):
|
||||||
|
self.target = target
|
||||||
|
self.manifest = manifest
|
||||||
|
self.sources = sources
|
||||||
|
self.resolved = resolved
|
||||||
|
|
||||||
|
def binfile(self) -> str:
|
||||||
|
return f"{self.target.builddir()}/bin/{self.manifest.id}.out"
|
||||||
|
|
||||||
|
def objdir(self) -> str:
|
||||||
|
return f"{self.target.builddir()}/obj/{self.manifest.id}"
|
||||||
|
|
||||||
|
def objsfiles(self) -> list[str]:
|
||||||
|
return list(
|
||||||
|
map(
|
||||||
|
lambda s: f"{self.objdir()}/{s}.o",
|
||||||
|
self.sources.remplace(self.manifest.dirname(), "")))
|
||||||
|
|
||||||
|
def libfile(self) -> str:
|
||||||
|
return f"{self.target.builddir()}/lib/{self.manifest.id}.a"
|
||||||
|
|
||||||
|
|
||||||
|
class Context:
|
||||||
|
target: TargetManifest
|
||||||
|
instances: list[ComponentInstance] = []
|
||||||
|
|
||||||
|
def __init__(self, target: TargetManifest, instances: list[ComponentInstance]):
|
||||||
|
self.target = target
|
||||||
|
self.instances = instances
|
||||||
|
|
||||||
|
|
||||||
|
def loadAllTargets() -> list[TargetManifest]:
|
||||||
|
files = shell.find(const.TARGETS_DIR, ["*.json"])
|
||||||
|
return list(
|
||||||
|
map(lambda path: TargetManifest(jexpr.evalRead(path), path), files))
|
||||||
|
|
||||||
|
|
||||||
|
def loadTarget(id: str) -> TargetManifest:
|
||||||
|
return next(filter(lambda t: t.id == id, loadAllTargets()))
|
||||||
|
|
||||||
|
|
||||||
|
def loadAllComponents() -> list[ComponentManifest]:
|
||||||
|
files = shell.find(const.SRC_DIR, ["manifest.json"])
|
||||||
|
return list(
|
||||||
|
map(
|
||||||
|
lambda path: ComponentManifest(jexpr.evalRead(path), path),
|
||||||
|
files))
|
||||||
|
|
||||||
|
|
||||||
|
def filterDisabled(components: list[ComponentManifest], target: TargetManifest) -> list[ComponentManifest]:
|
||||||
|
return list(filter(lambda c: c.isEnabled(target), components))
|
||||||
|
|
||||||
|
|
||||||
|
def providerFor(what: str, components: list[ComponentManifest]) -> str | None:
|
||||||
|
result: list[ComponentManifest] = list(
|
||||||
|
filter(lambda c: c.id == what, components))
|
||||||
|
|
||||||
|
if len(result) == 0:
|
||||||
|
# Try to find a provider
|
||||||
|
result = list(filter(lambda x: (what in x.provides), components))
|
||||||
|
|
||||||
|
if len(result) == 0:
|
||||||
|
logger.error(f"No provider for {what}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(result) > 1:
|
||||||
|
logger.error(f"Multiple providers for {what}: {result}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result[0].id
|
||||||
|
|
||||||
|
|
||||||
|
def resolveDeps(componentSpec: str, components: list[ComponentManifest], target: TargetManifest) -> tuple[bool, list[str]]:
|
||||||
|
mapping = dict(map(lambda c: (c.id, c), components))
|
||||||
|
|
||||||
|
def resolveInner(what: str, stack: list[str] = []) -> tuple[bool, list[str]]:
|
||||||
|
result: list[str] = []
|
||||||
|
what = target.route(what)
|
||||||
|
resolved = providerFor(what, components)
|
||||||
|
|
||||||
|
if resolved is None:
|
||||||
|
return False, []
|
||||||
|
|
||||||
|
if resolved in stack:
|
||||||
|
raise Exception(f"Dependency loop: {stack} -> {resolved}")
|
||||||
|
|
||||||
|
stack.append(resolved)
|
||||||
|
|
||||||
|
for req in mapping[resolved].requires:
|
||||||
|
keep, reqs = resolveInner(req, stack)
|
||||||
|
|
||||||
|
if not keep:
|
||||||
|
stack.pop()
|
||||||
|
logger.error(f"Dependency {req} not met for {resolved}")
|
||||||
|
return False, []
|
||||||
|
|
||||||
|
result.extend(reqs)
|
||||||
|
|
||||||
|
stack.pop()
|
||||||
|
result.append(resolved)
|
||||||
|
|
||||||
|
return True, result
|
||||||
|
|
||||||
|
enabled, resolved = resolveInner(componentSpec)
|
||||||
|
|
||||||
|
return enabled, resolved[1:]
|
||||||
|
|
||||||
|
|
||||||
|
def instanciate(componentSpec: str, components: list[ComponentManifest], target: TargetManifest) -> ComponentInstance:
|
||||||
|
manifest = next(filter(lambda c: c.id == componentSpec, components))
|
||||||
|
sources = shell.find(
|
||||||
|
manifest.path, ["*.c", "*.cpp", "*.s", "*.asm"])
|
||||||
|
enabled, resolved = resolveDeps(componentSpec, components, target)
|
||||||
|
|
||||||
|
return ComponentInstance(target, manifest, sources, resolved)
|
||||||
|
|
||||||
|
|
||||||
|
def contextFor(targetSpec: str, props: Props) -> Context:
|
||||||
|
logger.log(f"Loading context for {targetSpec}")
|
||||||
|
|
||||||
|
target = loadTarget(targetSpec)
|
||||||
|
components = loadAllComponents()
|
||||||
|
|
||||||
|
components = filterDisabled(components, target)
|
||||||
|
instances = list(map(lambda c: instanciate(
|
||||||
|
c.id, components, target), components))
|
||||||
|
|
||||||
|
return Context(
|
||||||
|
target,
|
||||||
|
instances
|
||||||
|
)
|
51
osdk/jexpr.py
Normal file
51
osdk/jexpr.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
from typing import Any, cast, Callable, Final
|
||||||
|
import json
|
||||||
|
|
||||||
|
import osdk.shell as shell
|
||||||
|
|
||||||
|
Json = Any
|
||||||
|
Builtin = Callable[..., Json]
|
||||||
|
|
||||||
|
BUILTINS: Final[dict[str, Builtin]] = {
|
||||||
|
"uname": lambda arg: getattr(shell.uname(), arg).lower(),
|
||||||
|
"include": lambda arg: evalRead(arg),
|
||||||
|
"evalRead": lambda arg: evalRead(arg),
|
||||||
|
"join": lambda lhs, rhs: cast(Json, {**lhs, **rhs} if isinstance(lhs, dict) else lhs + rhs),
|
||||||
|
"concat": lambda *args: "".join(args),
|
||||||
|
"eval": lambda arg: eval(arg),
|
||||||
|
"read": lambda arg: read(arg),
|
||||||
|
"exec": lambda *args: shell.popen(*args).splitlines(),
|
||||||
|
"latest": lambda arg: shell.latest(arg),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def eval(jexpr: Json) -> Json:
|
||||||
|
if isinstance(jexpr, dict):
|
||||||
|
result = {}
|
||||||
|
for k in cast(dict[str, Json], jexpr):
|
||||||
|
result[k] = eval(jexpr[k])
|
||||||
|
return cast(Json, result)
|
||||||
|
elif isinstance(jexpr, list):
|
||||||
|
jexpr = cast(list[Json], jexpr)
|
||||||
|
if len(jexpr) > 0 and isinstance(jexpr[0], str) and jexpr[0].startswith("@"):
|
||||||
|
funcName = jexpr[0][1:]
|
||||||
|
if funcName in BUILTINS:
|
||||||
|
return BUILTINS[funcName](*eval(jexpr[1:]))
|
||||||
|
|
||||||
|
raise Exception(f"Unknown macro {funcName}")
|
||||||
|
else:
|
||||||
|
return list(map(eval, jexpr))
|
||||||
|
else:
|
||||||
|
return jexpr
|
||||||
|
|
||||||
|
|
||||||
|
def read(path: str) -> Json:
|
||||||
|
try:
|
||||||
|
with open(path, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except:
|
||||||
|
raise Exception(f"Failed to read {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def evalRead(path: str) -> Json:
|
||||||
|
return eval(read(path))
|
17
osdk/logger.py
Normal file
17
osdk/logger.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import osdk.vt100 as vt100
|
||||||
|
|
||||||
|
|
||||||
|
class Logger:
|
||||||
|
name: str
|
||||||
|
|
||||||
|
def __init__(self, name: str):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def log(self, message: str):
|
||||||
|
print(f"{vt100.CYAN}[{self.name}]{vt100.RESET} {message}")
|
||||||
|
|
||||||
|
def warn(self, message: str):
|
||||||
|
print(f"{vt100.YELLOW}[{self.name}]{vt100.RESET} {message}")
|
||||||
|
|
||||||
|
def error(self, message: str):
|
||||||
|
print(f"{vt100.RED}[{self.name}]{vt100.RESET} {message}")
|
191
osdk/model.py
Normal file
191
osdk/model.py
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
import os
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
from json import JSONEncoder
|
||||||
|
|
||||||
|
from osdk.jexpr import Json, evalRead
|
||||||
|
from osdk.logger import Logger
|
||||||
|
from osdk import shell, const, utils
|
||||||
|
|
||||||
|
|
||||||
|
logger = Logger("model")
|
||||||
|
|
||||||
|
Props = dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class Type(Enum):
|
||||||
|
TARGET = "target"
|
||||||
|
LIB = "lib"
|
||||||
|
EXE = "exe"
|
||||||
|
|
||||||
|
|
||||||
|
class Manifest:
|
||||||
|
id: str
|
||||||
|
type: Type
|
||||||
|
path: str = ""
|
||||||
|
|
||||||
|
def __init__(self, json: Json, path: str):
|
||||||
|
if not "id" in json:
|
||||||
|
raise ValueError("Missing id")
|
||||||
|
|
||||||
|
self.id = json["id"]
|
||||||
|
|
||||||
|
if not "type" in json:
|
||||||
|
raise ValueError("Missing type")
|
||||||
|
|
||||||
|
self.type = Type(json["type"])
|
||||||
|
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Manifest(id={self.id}, type={self.type}, path={self.path})"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Manifest({id})"
|
||||||
|
|
||||||
|
def dirname(self) -> str:
|
||||||
|
return os.path.dirname(self.path)
|
||||||
|
|
||||||
|
|
||||||
|
class Tool:
|
||||||
|
cmd: str
|
||||||
|
args: list[str]
|
||||||
|
files: list[str] = []
|
||||||
|
|
||||||
|
def __init__(self, json: Json):
|
||||||
|
if not "cmd" in json:
|
||||||
|
raise ValueError("Missing cmd")
|
||||||
|
self.cmd = json["cmd"]
|
||||||
|
|
||||||
|
if not "args" in json:
|
||||||
|
raise ValueError("Missing args")
|
||||||
|
self.args = json["args"]
|
||||||
|
|
||||||
|
self.files = json.get("files", [])
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Tool(cmd={self.cmd}, args={self.args})"
|
||||||
|
|
||||||
|
|
||||||
|
class TargetManifest(Manifest):
|
||||||
|
props: Props
|
||||||
|
tools: dict[str, Tool]
|
||||||
|
routing: dict[str, str]
|
||||||
|
|
||||||
|
def __init__(self, json: Json, path: str):
|
||||||
|
super().__init__(json, path)
|
||||||
|
|
||||||
|
if not "props" in json:
|
||||||
|
raise ValueError("Missing props")
|
||||||
|
|
||||||
|
self.props = json["props"]
|
||||||
|
|
||||||
|
if not "tools" in json:
|
||||||
|
raise ValueError("Missing tools")
|
||||||
|
|
||||||
|
self.tools = {k: Tool(v) for k, v in json["tools"].items()}
|
||||||
|
|
||||||
|
self.routing = json.get("routing", {})
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"TargetManifest(" + \
|
||||||
|
"id={self.id}, " + \
|
||||||
|
"type={self.type}, " + \
|
||||||
|
"props={self.props}, " + \
|
||||||
|
"tools={self.tools}, " + \
|
||||||
|
"path={self.path}" + \
|
||||||
|
")"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"TargetManifest({self.id})"
|
||||||
|
|
||||||
|
def route(self, componentSpec: str):
|
||||||
|
return self.routing[componentSpec] if componentSpec in self.routing else componentSpec
|
||||||
|
|
||||||
|
def hashid(self) -> str:
|
||||||
|
return utils.hash((self.props, self.tools), cls=ModelEncoder)
|
||||||
|
|
||||||
|
def builddir(self) -> str:
|
||||||
|
return f"{const.BUILD_DIR}/{self.id}-{self.hashid()[:8]}"
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentManifest(Manifest):
|
||||||
|
decription: str
|
||||||
|
props: Props
|
||||||
|
enableIf: dict[str, list[Any]]
|
||||||
|
requires: list[str]
|
||||||
|
provides: list[str]
|
||||||
|
|
||||||
|
def __init__(self, json: Json, path: str):
|
||||||
|
super().__init__(json, path)
|
||||||
|
|
||||||
|
self.decription = json.get("description", "(No description)")
|
||||||
|
self.props = json.get("props", {})
|
||||||
|
self.enableIf = json.get("enableIf", {})
|
||||||
|
self.requires = json.get("requires", [])
|
||||||
|
self.provides = json.get("provides", [])
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"ComponentManifest(" + \
|
||||||
|
"id={self.id}, " + \
|
||||||
|
"type={self.type}, " + \
|
||||||
|
"description={self.decription}, " + \
|
||||||
|
"requires={self.requires}, " + \
|
||||||
|
"provides={self.provides}, " + \
|
||||||
|
"injects={self.injects}, " + \
|
||||||
|
"deps={self.deps}, " + \
|
||||||
|
"path={self.path})"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"ComponentManifest({self.id})"
|
||||||
|
|
||||||
|
def isEnabled(self, target: TargetManifest):
|
||||||
|
for k, v in self.enableIf.items():
|
||||||
|
if k in target.props and target.props[k] in v:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ModelEncoder(JSONEncoder):
|
||||||
|
def default(self, o):
|
||||||
|
if isinstance(o, Manifest):
|
||||||
|
return {
|
||||||
|
"id": o.id,
|
||||||
|
"type": o.type.value,
|
||||||
|
"path": o.path
|
||||||
|
}
|
||||||
|
|
||||||
|
if isinstance(o, Type):
|
||||||
|
return o.value
|
||||||
|
|
||||||
|
if isinstance(o, Tool):
|
||||||
|
return {
|
||||||
|
"cmd": o.cmd,
|
||||||
|
"args": o.args,
|
||||||
|
"files": o.files
|
||||||
|
}
|
||||||
|
|
||||||
|
if isinstance(o, TargetManifest):
|
||||||
|
return {
|
||||||
|
"id": o.id,
|
||||||
|
"type": o.type.value,
|
||||||
|
"props": o.props,
|
||||||
|
"tools": o.tools,
|
||||||
|
"routing": o.routing,
|
||||||
|
"path": o.path
|
||||||
|
}
|
||||||
|
|
||||||
|
if isinstance(o, ComponentManifest):
|
||||||
|
return {
|
||||||
|
"id": o.id,
|
||||||
|
"type": o.type.value,
|
||||||
|
"description": o.decription,
|
||||||
|
"props": o.props,
|
||||||
|
"enableIf": o.enableIf,
|
||||||
|
"requires": o.requires,
|
||||||
|
"provides": o.provides,
|
||||||
|
"path": o.path
|
||||||
|
}
|
||||||
|
|
||||||
|
return super().default(o)
|
|
@ -22,12 +22,13 @@ just a helpful utility for build-file-generation systems that already
|
||||||
use Python.
|
use Python.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import Any, TextIO, Union
|
from typing import TextIO, Union
|
||||||
|
|
||||||
|
from osdk.utils import asList
|
||||||
|
|
||||||
|
|
||||||
def escape_path(word: str) -> str:
|
def escapePath(word: str) -> str:
|
||||||
return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')
|
return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,6 +49,10 @@ 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:
|
||||||
|
self.output.write(f"# --- {text} ---" + '-' *
|
||||||
|
(self.width - 10 - len(text)) + " #\n\n")
|
||||||
|
|
||||||
def variable(self, key: str, value: VarValue, indent: int = 0) -> None:
|
def variable(self, key: str, value: VarValue, indent: int = 0) -> None:
|
||||||
if value is None:
|
if value is None:
|
||||||
return
|
return
|
||||||
|
@ -99,21 +104,21 @@ class Writer(object):
|
||||||
implicit_outputs: VarPath = None,
|
implicit_outputs: VarPath = None,
|
||||||
pool: Union[str, None] = None,
|
pool: Union[str, None] = None,
|
||||||
dyndep: Union[str, None] = None) -> list[str]:
|
dyndep: Union[str, None] = None) -> list[str]:
|
||||||
outputs = as_list(outputs)
|
outputs = asList(outputs)
|
||||||
out_outputs = [escape_path(x) for x in outputs]
|
out_outputs = [escapePath(x) for x in outputs]
|
||||||
all_inputs = [escape_path(x) for x in as_list(inputs)]
|
all_inputs = [escapePath(x) for x in asList(inputs)]
|
||||||
|
|
||||||
if implicit:
|
if implicit:
|
||||||
implicit = [escape_path(x) for x in as_list(implicit)]
|
implicit = [escapePath(x) for x in asList(implicit)]
|
||||||
all_inputs.append('|')
|
all_inputs.append('|')
|
||||||
all_inputs.extend(implicit)
|
all_inputs.extend(implicit)
|
||||||
if order_only:
|
if order_only:
|
||||||
order_only = [escape_path(x) for x in as_list(order_only)]
|
order_only = [escapePath(x) for x in asList(order_only)]
|
||||||
all_inputs.append('||')
|
all_inputs.append('||')
|
||||||
all_inputs.extend(order_only)
|
all_inputs.extend(order_only)
|
||||||
if implicit_outputs:
|
if implicit_outputs:
|
||||||
implicit_outputs = [escape_path(x)
|
implicit_outputs = [escapePath(x)
|
||||||
for x in as_list(implicit_outputs)]
|
for x in asList(implicit_outputs)]
|
||||||
out_outputs.append('|')
|
out_outputs.append('|')
|
||||||
out_outputs.extend(implicit_outputs)
|
out_outputs.extend(implicit_outputs)
|
||||||
|
|
||||||
|
@ -139,7 +144,7 @@ class Writer(object):
|
||||||
self._line('subninja %s' % path)
|
self._line('subninja %s' % path)
|
||||||
|
|
||||||
def default(self, paths: VarPath) -> None:
|
def default(self, paths: VarPath) -> None:
|
||||||
self._line('default %s' % ' '.join(as_list(paths)))
|
self._line('default %s' % ' '.join(asList(paths)))
|
||||||
|
|
||||||
def _count_dollars_before_index(self, s: str, i: int) -> int:
|
def _count_dollars_before_index(self, s: str, i: int) -> int:
|
||||||
"""Returns the number of '$' characters right in front of s[i]."""
|
"""Returns the number of '$' characters right in front of s[i]."""
|
||||||
|
@ -190,31 +195,9 @@ class Writer(object):
|
||||||
self.output.close()
|
self.output.close()
|
||||||
|
|
||||||
|
|
||||||
def as_list(input: Any) -> list:
|
|
||||||
if input is None:
|
|
||||||
return []
|
|
||||||
if isinstance(input, list):
|
|
||||||
return input
|
|
||||||
return [input]
|
|
||||||
|
|
||||||
|
|
||||||
def escape(string: str) -> str:
|
def escape(string: str) -> str:
|
||||||
"""Escape a string such that it can be embedded into a Ninja file without
|
"""Escape a string such that it can be embedded into a Ninja file without
|
||||||
further interpretation."""
|
further interpretation."""
|
||||||
assert '\n' not in string, 'Ninja syntax does not allow newlines'
|
assert '\n' not in string, 'Ninja syntax does not allow newlines'
|
||||||
# We only have one special metacharacter: '$'.
|
# We only have one special metacharacter: '$'.
|
||||||
return string.replace('$', '$$')
|
return string.replace('$', '$$')
|
||||||
|
|
||||||
|
|
||||||
def expand(string: str, vars: dict[str, str], local_vars: dict[str, str] = {}) -> str:
|
|
||||||
"""Expand a string containing $vars as Ninja would.
|
|
||||||
|
|
||||||
Note: doesn't handle the full Ninja variable syntax, but it's enough
|
|
||||||
to make configure.py's use of it work.
|
|
||||||
"""
|
|
||||||
def exp(m: Any) -> Any:
|
|
||||||
var = m.group(1)
|
|
||||||
if var == '$':
|
|
||||||
return '$'
|
|
||||||
return local_vars.get(var, vars.get(var, ''))
|
|
||||||
return re.sub(r'\$(\$|\w*)', exp, string)
|
|
||||||
|
|
28
osdk/plugins.py
Normal file
28
osdk/plugins.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import importlib.util as importlib
|
||||||
|
from osdk.logger import Logger
|
||||||
|
from osdk.shell import readdir
|
||||||
|
|
||||||
|
logger = Logger("plugins")
|
||||||
|
|
||||||
|
|
||||||
|
def load(path: str):
|
||||||
|
logger.log(f"Loading plugin {path}")
|
||||||
|
spec = importlib.spec_from_file_location("plugin", path)
|
||||||
|
|
||||||
|
if not spec or not spec.loader:
|
||||||
|
logger.error(f"Failed to load plugin {path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
module = importlib.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
|
||||||
|
def loadAll():
|
||||||
|
logger.log("Loading plugins...")
|
||||||
|
for files in readdir("meta/plugins"):
|
||||||
|
if files.endswith(".py"):
|
||||||
|
plugin = load(f"meta/plugins/{files}")
|
||||||
|
|
||||||
|
if plugin:
|
||||||
|
print(f"Loaded plugin {plugin.name}")
|
||||||
|
plugin.init()
|
7
osdk/rules.py
Normal file
7
osdk/rules.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
rules = {
|
||||||
|
"cc": "-c -o $out $in -MD -MF $out.d $flags",
|
||||||
|
"cxx": "-c -o $out $in -MD -MF $out.d $flags",
|
||||||
|
"as": "-o $out $in $flags",
|
||||||
|
"ar": "$flags $out $in",
|
||||||
|
"ld": "-o $out $in $flags",
|
||||||
|
}
|
194
osdk/shell.py
Normal file
194
osdk/shell.py
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
import errno
|
||||||
|
import subprocess
|
||||||
|
import signal
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import fnmatch
|
||||||
|
|
||||||
|
from osdk.logger import Logger
|
||||||
|
from osdk import const
|
||||||
|
|
||||||
|
logger = Logger("shell")
|
||||||
|
|
||||||
|
|
||||||
|
class Uname:
|
||||||
|
def __init__(self, sysname: str, nodename: str, release: str, version: str, machine: str):
|
||||||
|
self.sysname = sysname
|
||||||
|
self.nodename = nodename
|
||||||
|
self.release = release
|
||||||
|
self.version = version
|
||||||
|
self.machine = machine
|
||||||
|
|
||||||
|
|
||||||
|
def uname() -> Uname:
|
||||||
|
un = os.uname()
|
||||||
|
result = Uname(un.sysname, un.nodename, un.release, un.version, un.machine)
|
||||||
|
if result.machine == "aarch64":
|
||||||
|
result.machine = "arm64"
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def sha256sum(path: str) -> str:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
return hashlib.sha256(f.read()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def find(path: str, wildcards: list[str] = []) -> list[str]:
|
||||||
|
logger.log(f"Looking for files in {path} matching {wildcards}")
|
||||||
|
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
return []
|
||||||
|
|
||||||
|
result: list[str] = []
|
||||||
|
|
||||||
|
for root, _, files in os.walk(path):
|
||||||
|
for f in files:
|
||||||
|
if len(wildcards) == 0:
|
||||||
|
result.append(os.path.join(root, f))
|
||||||
|
else:
|
||||||
|
for wildcard in wildcards:
|
||||||
|
if fnmatch.fnmatch(f, wildcard):
|
||||||
|
result.append(os.path.join(root, f))
|
||||||
|
break
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def mkdir(path: str) -> str:
|
||||||
|
logger.log(f"Creating directory {path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(path)
|
||||||
|
except OSError as exc:
|
||||||
|
if not (exc.errno == errno.EEXIST and os.path.isdir(path)):
|
||||||
|
raise
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def rmrf(path: str) -> bool:
|
||||||
|
logger.log(f"Removing directory {path}")
|
||||||
|
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return False
|
||||||
|
shutil.rmtree(path, ignore_errors=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def wget(url: str, path: str | None = None) -> str:
|
||||||
|
import requests
|
||||||
|
|
||||||
|
if path is None:
|
||||||
|
path = const.CACHE_DIR + "/" + \
|
||||||
|
hashlib.sha256(url.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
if os.path.exists(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
logger.log(f"Downloading {url} to {path}")
|
||||||
|
|
||||||
|
r = requests.get(url, stream=True)
|
||||||
|
r.raise_for_status()
|
||||||
|
mkdir(os.path.dirname(path))
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
for chunk in r.iter_content(chunk_size=8192):
|
||||||
|
if chunk:
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def exec(*args: str):
|
||||||
|
logger.log(f"Executing {args}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(args)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise Exception(f"Command not found")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
raise Exception("Interrupted")
|
||||||
|
|
||||||
|
if proc.returncode == -signal.SIGSEGV:
|
||||||
|
raise Exception("Segmentation fault")
|
||||||
|
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise Exception(f"Process exited with code {proc.returncode}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def popen(*args: str) -> str:
|
||||||
|
logger.log(f"Executing {args}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(args, stdout=subprocess.PIPE)
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise Exception(f"Command not found")
|
||||||
|
|
||||||
|
if proc.returncode == -signal.SIGSEGV:
|
||||||
|
raise Exception("Segmentation fault")
|
||||||
|
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise Exception(f"Process exited with code {proc.returncode}")
|
||||||
|
|
||||||
|
return proc.stdout.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def readdir(path: str) -> list[str]:
|
||||||
|
logger.log(f"Reading directory {path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return os.listdir(path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def cp(src: str, dst: str):
|
||||||
|
logger.log(f"Copying {src} to {dst}")
|
||||||
|
|
||||||
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
LATEST_CACHE: dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def latest(cmd: str) -> str:
|
||||||
|
"""
|
||||||
|
Find the latest version of a command
|
||||||
|
|
||||||
|
Exemples
|
||||||
|
clang -> clang-15
|
||||||
|
clang++ -> clang++-15
|
||||||
|
gcc -> gcc10
|
||||||
|
"""
|
||||||
|
|
||||||
|
global LATEST_CACHE
|
||||||
|
|
||||||
|
if cmd in LATEST_CACHE:
|
||||||
|
return LATEST_CACHE[cmd]
|
||||||
|
|
||||||
|
logger.log(f"Finding latest version of {cmd}")
|
||||||
|
|
||||||
|
regex = re.compile(r"^" + re.escape(cmd) + r"(-.[0-9]+)?$")
|
||||||
|
|
||||||
|
versions: list[str] = []
|
||||||
|
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 Exception(f"{cmd} not found")
|
||||||
|
|
||||||
|
versions.sort()
|
||||||
|
chosen = versions[-1]
|
||||||
|
|
||||||
|
logger.log(f"Chosen {chosen} as latest version of {cmd}")
|
||||||
|
|
||||||
|
LATEST_CACHE[cmd] = chosen
|
||||||
|
|
||||||
|
return chosen
|
264
osdk/utils.py
264
osdk/utils.py
|
@ -1,103 +1,37 @@
|
||||||
from copy import copy
|
from typing import Any, TypeVar, cast
|
||||||
import errno
|
|
||||||
import os
|
|
||||||
import hashlib
|
|
||||||
import signal
|
|
||||||
import requests
|
|
||||||
import subprocess
|
|
||||||
import json
|
import json
|
||||||
import copy
|
import hashlib
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class Colors:
|
def uniq(l: list[str]) -> list[str]:
|
||||||
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] = []
|
result: list[str] = []
|
||||||
for item in l:
|
for i in l:
|
||||||
if item in result:
|
if i in result:
|
||||||
result.remove(item)
|
result.remove(i)
|
||||||
result.append(item)
|
result.append(i)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def findFiles(dir: str, exts: list[str] = []) -> list[str]:
|
def hash(obj: Any, keys: list[str] = [], cls: type[json.JSONEncoder] | None = None) -> 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 = {}
|
toHash = {}
|
||||||
|
|
||||||
if len(keys) == 0:
|
if len(keys) == 0:
|
||||||
toHash = obj
|
toHash = obj
|
||||||
else:
|
else:
|
||||||
for key in keys:
|
for key in keys:
|
||||||
if key in obj:
|
if key in obj:
|
||||||
toHash[key] = obj[key]
|
toHash[key] = obj[key]
|
||||||
|
data = json.dumps(toHash, sort_keys=True, cls=cls)
|
||||||
data = json.dumps(toHash, sort_keys=True)
|
|
||||||
return hashlib.sha256(data.encode("utf-8")).hexdigest()
|
return hashlib.sha256(data.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def toCamelCase(s: str) -> str:
|
def camelCase(s: str) -> str:
|
||||||
s = ''.join(x for x in s.title() if x != '_' and x != '-')
|
s = ''.join(x for x in s.title() if x != '_' and x != '-')
|
||||||
s = s[0].lower() + s[1:]
|
s = s[0].lower() + s[1:]
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
def objKey(obj: dict, keys: list[str] = []) -> str:
|
def key(obj: Any, keys: list[str] = []) -> str:
|
||||||
toKey = []
|
k: list[str] = []
|
||||||
|
|
||||||
if len(keys) == 0:
|
if len(keys) == 0:
|
||||||
keys = list(obj.keys())
|
keys = list(obj.keys())
|
||||||
|
@ -107,175 +41,19 @@ def objKey(obj: dict, keys: list[str] = []) -> str:
|
||||||
if key in obj:
|
if key in obj:
|
||||||
if isinstance(obj[key], bool):
|
if isinstance(obj[key], bool):
|
||||||
if obj[key]:
|
if obj[key]:
|
||||||
toKey.append(key)
|
k.append(key)
|
||||||
else:
|
else:
|
||||||
toKey.append(f"{toCamelCase(key)}({obj[key]})")
|
k.append(f"{camelCase(key)}({obj[key]})")
|
||||||
|
|
||||||
return "-".join(toKey)
|
return "-".join(k)
|
||||||
|
|
||||||
|
|
||||||
def mkdirP(path: str) -> str:
|
T = TypeVar('T')
|
||||||
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:
|
def asList(i: T | list[T] | None) -> list[T]:
|
||||||
dest = ".osdk/cache/" + hashlib.sha256(url.encode('utf-8')).hexdigest()
|
if i is None:
|
||||||
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 []
|
return []
|
||||||
|
if isinstance(i, list):
|
||||||
|
return cast(list[T], i)
|
||||||
|
return [i]
|
||||||
|
|
47
osdk/vt100.py
Normal file
47
osdk/vt100.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
def title(text: str):
|
||||||
|
print(f"{LIGHT_WHITE}{text}{RESET}:")
|
||||||
|
|
||||||
|
|
||||||
|
def wordwrap(text: str, width: int = 60) -> str:
|
||||||
|
result = ""
|
||||||
|
curr = 0
|
||||||
|
|
||||||
|
for c in text:
|
||||||
|
if c == " " and curr > width:
|
||||||
|
result += "\n"
|
||||||
|
curr = 0
|
||||||
|
else:
|
||||||
|
result += c
|
||||||
|
curr += 1
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def indent(text: str, indent: int = 4) -> str:
|
||||||
|
return " " * indent + text.replace("\n", "\n" + " " * indent)
|
4
setup.py
4
setup.py
|
@ -1,8 +1,9 @@
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
from osdk.const import VERSION
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="osdk",
|
name="osdk",
|
||||||
version="0.3.2",
|
version=VERSION,
|
||||||
python_requires='>=3.10',
|
python_requires='>=3.10',
|
||||||
description="Operating System Development Kit",
|
description="Operating System Development Kit",
|
||||||
author="The DEVSE Community",
|
author="The DEVSE Community",
|
||||||
|
@ -11,6 +12,7 @@ setup(
|
||||||
packages=["osdk"],
|
packages=["osdk"],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"requests",
|
"requests",
|
||||||
|
"graphviz",
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
"console_scripts": [
|
"console_scripts": [
|
||||||
|
|
Loading…
Add table
Reference in a new issue