Rewritten using typed python and simplified the model.
This commit is contained in:
parent
0dd7653de2
commit
a3ae84fde9
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)
|
||||
if not keep:
|
||||
stack.pop()
|
||||
print(f"Disabling {key} because we are missing a deps")
|
||||
return False, "", []
|
||||
result.append(dep)
|
||||
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
|
||||
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:
|
||||
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
|
||||
from osdk.args import parse
|
||||
from osdk.cmds import exec, usage
|
||||
from osdk.plugins import loadAll
|
||||
import osdk.vt100 as vt100
|
||||
|
||||
|
||||
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 = 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
|
||||
def main() -> int:
|
||||
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:
|
||||
loadAll()
|
||||
a = parse(sys.argv[1:])
|
||||
exec(a)
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"{vt100.RED}{e}{vt100.RESET}")
|
||||
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.
|
||||
"""
|
||||
|
||||
import re
|
||||
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(':', '$:')
|
||||
|
||||
|
||||
|
@ -48,6 +49,10 @@ class Writer(object):
|
|||
break_on_hyphens=False):
|
||||
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:
|
||||
if value is None:
|
||||
return
|
||||
|
@ -99,21 +104,21 @@ class Writer(object):
|
|||
implicit_outputs: VarPath = None,
|
||||
pool: Union[str, None] = None,
|
||||
dyndep: Union[str, None] = None) -> list[str]:
|
||||
outputs = as_list(outputs)
|
||||
out_outputs = [escape_path(x) for x in outputs]
|
||||
all_inputs = [escape_path(x) for x in as_list(inputs)]
|
||||
outputs = asList(outputs)
|
||||
out_outputs = [escapePath(x) for x in outputs]
|
||||
all_inputs = [escapePath(x) for x in asList(inputs)]
|
||||
|
||||
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.extend(implicit)
|
||||
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.extend(order_only)
|
||||
if implicit_outputs:
|
||||
implicit_outputs = [escape_path(x)
|
||||
for x in as_list(implicit_outputs)]
|
||||
implicit_outputs = [escapePath(x)
|
||||
for x in asList(implicit_outputs)]
|
||||
out_outputs.append('|')
|
||||
out_outputs.extend(implicit_outputs)
|
||||
|
||||
|
@ -139,7 +144,7 @@ class Writer(object):
|
|||
self._line('subninja %s' % path)
|
||||
|
||||
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:
|
||||
"""Returns the number of '$' characters right in front of s[i]."""
|
||||
|
@ -190,31 +195,9 @@ class Writer(object):
|
|||
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:
|
||||
"""Escape a string such that it can be embedded into a Ninja file without
|
||||
further interpretation."""
|
||||
assert '\n' not in string, 'Ninja syntax does not allow newlines'
|
||||
# We only have one special metacharacter: '$'.
|
||||
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
|
||||
import errno
|
||||
import os
|
||||
import hashlib
|
||||
import signal
|
||||
import requests
|
||||
import subprocess
|
||||
from typing import Any, TypeVar, cast
|
||||
import json
|
||||
import copy
|
||||
import re
|
||||
import hashlib
|
||||
|
||||
|
||||
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
|
||||
def uniq(l: list[str]) -> list[str]:
|
||||
result: list[str] = []
|
||||
for item in l:
|
||||
if item in result:
|
||||
result.remove(item)
|
||||
result.append(item)
|
||||
for i in l:
|
||||
if i in result:
|
||||
result.remove(i)
|
||||
result.append(i)
|
||||
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:
|
||||
def hash(obj: Any, keys: list[str] = [], cls: type[json.JSONEncoder] | None = None) -> 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)
|
||||
data = json.dumps(toHash, sort_keys=True, cls=cls)
|
||||
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 = s[0].lower() + s[1:]
|
||||
return s
|
||||
|
||||
|
||||
def objKey(obj: dict, keys: list[str] = []) -> str:
|
||||
toKey = []
|
||||
def key(obj: Any, keys: list[str] = []) -> str:
|
||||
k: list[str] = []
|
||||
|
||||
if len(keys) == 0:
|
||||
keys = list(obj.keys())
|
||||
|
@ -107,175 +41,19 @@ def objKey(obj: dict, keys: list[str] = []) -> str:
|
|||
if key in obj:
|
||||
if isinstance(obj[key], bool):
|
||||
if obj[key]:
|
||||
toKey.append(key)
|
||||
k.append(key)
|
||||
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:
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.EEXIST and os.path.isdir(path):
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
return path
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
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:
|
||||
def asList(i: T | list[T] | None) -> list[T]:
|
||||
if i is None:
|
||||
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 osdk.const import VERSION
|
||||
|
||||
setup(
|
||||
name="osdk",
|
||||
version="0.3.2",
|
||||
version=VERSION,
|
||||
python_requires='>=3.10',
|
||||
description="Operating System Development Kit",
|
||||
author="The DEVSE Community",
|
||||
|
@ -11,6 +12,7 @@ setup(
|
|||
packages=["osdk"],
|
||||
install_requires=[
|
||||
"requests",
|
||||
"graphviz",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
|
|
Loading…
Reference in a new issue