Rewritten using typed python and simplified the model.

This commit is contained in:
Nicolas Van Bossuyt 2023-01-30 10:39:49 +01:00
parent 0dd7653de2
commit a3ae84fde9
23 changed files with 1478 additions and 486 deletions

216
osdk-old/__init__.py Normal file
View 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
View file

@ -0,0 +1,5 @@
import sys
from . import main
sys.exit(main())

View file

@ -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
View 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 []

View file

@ -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
View file

42
osdk/args.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)

View file

@ -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
View 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
View 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
View 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

View file

@ -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
View 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)

View file

@ -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": [