morvix 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- morvix/__init__.py +10 -0
- morvix/__main__.py +8 -0
- morvix/adapters/__init__.py +117 -0
- morvix/adapters/c.py +85 -0
- morvix/adapters/cpp.py +83 -0
- morvix/adapters/java.py +108 -0
- morvix/adapters/nasm.py +94 -0
- morvix/adapters/python.py +53 -0
- morvix/adapters/rust.py +70 -0
- morvix/app.py +75 -0
- morvix/banner.py +22 -0
- morvix/cases.py +129 -0
- morvix/commands/__init__.py +1 -0
- morvix/commands/bruteforce_cmd.py +45 -0
- morvix/commands/clean_cmd.py +53 -0
- morvix/commands/config_cmd.py +179 -0
- morvix/commands/exit_cmd.py +14 -0
- morvix/commands/gen_cmd.py +219 -0
- morvix/commands/help_cmd.py +99 -0
- morvix/commands/import_cmd.py +86 -0
- morvix/commands/init_cmd.py +111 -0
- morvix/commands/model_cmd.py +56 -0
- morvix/commands/open_cmd.py +69 -0
- morvix/commands/package_cmd.py +109 -0
- morvix/commands/reference_cmd.py +60 -0
- morvix/commands/result_cmd.py +228 -0
- morvix/commands/run_cmd.py +139 -0
- morvix/commands/runner_cmd.py +327 -0
- morvix/commands/status_cmd.py +75 -0
- morvix/commands/workflow_cmd.py +140 -0
- morvix/comparators.py +108 -0
- morvix/compare.py +110 -0
- morvix/components/__init__.py +1 -0
- morvix/components/choice.py +33 -0
- morvix/components/confirm.py +41 -0
- morvix/components/form.py +254 -0
- morvix/components/progress.py +45 -0
- morvix/components/selection.py +235 -0
- morvix/components/table.py +114 -0
- morvix/context.py +74 -0
- morvix/errors.py +45 -0
- morvix/execmodels/__init__.py +3 -0
- morvix/execmodels/args.py +36 -0
- morvix/execmodels/file.py +60 -0
- morvix/execmodels/interactive.py +157 -0
- morvix/execmodels/library.py +119 -0
- morvix/generators.py +313 -0
- morvix/help_text.py +178 -0
- morvix/judge.py +203 -0
- morvix/layout.py +48 -0
- morvix/manifest.py +95 -0
- morvix/messages.py +57 -0
- morvix/models.py +124 -0
- morvix/packaging.py +167 -0
- morvix/process.py +337 -0
- morvix/project.py +250 -0
- morvix/readme.py +151 -0
- morvix/registry.py +178 -0
- morvix/results.py +161 -0
- morvix/runner_build.py +82 -0
- morvix/runner_core/morvix_runner.py +1245 -0
- morvix/runner_core/run.sh +36 -0
- morvix/shapes.py +249 -0
- morvix/shell.py +196 -0
- morvix/suggestions.py +113 -0
- morvix/theme.py +75 -0
- morvix/version.py +7 -0
- morvix/workflow.py +89 -0
- morvix-0.1.0.dist-info/METADATA +126 -0
- morvix-0.1.0.dist-info/RECORD +73 -0
- morvix-0.1.0.dist-info/WHEEL +4 -0
- morvix-0.1.0.dist-info/entry_points.txt +2 -0
- morvix-0.1.0.dist-info/licenses/LICENSE +21 -0
morvix/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Morvix - a test-authoring, test-running, and test-sharing CLI for
|
|
2
|
+
# university programming assignments.
|
|
3
|
+
#
|
|
4
|
+
# This package is the tool itself. The stdlib-only runner that ships inside
|
|
5
|
+
# shared packages lives in morvix/runner_core/ and is deliberately kept
|
|
6
|
+
# separate so it never imports anything from here.
|
|
7
|
+
|
|
8
|
+
from morvix.version import __version__
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
morvix/__main__.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Language adapters: the only language-aware part of the system (Section 8).
|
|
2
|
+
#
|
|
3
|
+
# An adapter knows one thing - how to turn this language's source into something
|
|
4
|
+
# runnable, and how to invoke it. It knows nothing about tests, models or
|
|
5
|
+
# comparison. That is the whole point: adding a language is writing one adapter
|
|
6
|
+
# (Section 4.1), and nothing else in the tool changes.
|
|
7
|
+
#
|
|
8
|
+
# Each concrete adapter lives in its own module (c.py, cpp.py, ...) and calls
|
|
9
|
+
# register() at import time. The registry below is how the rest of the tool
|
|
10
|
+
# finds them.
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BuildResult:
|
|
19
|
+
"""The outcome of compiling/assembling a solution.
|
|
20
|
+
|
|
21
|
+
On success, artifact + the run information are filled in. On failure, ok is
|
|
22
|
+
False and diagnostics holds the toolchain's own output, shown verbatim.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
ok: bool
|
|
26
|
+
artifact: Optional[str] = None # the produced binary / class dir / script path
|
|
27
|
+
run_argv: List[str] = field(default_factory=list) # how to invoke the artifact
|
|
28
|
+
run_env: Dict[str, str] = field(default_factory=dict) # extra env (LD_LIBRARY_PATH, ...)
|
|
29
|
+
diagnostics: str = "" # compiler output, shown on failure
|
|
30
|
+
error: Optional[str] = None # short summary of what went wrong
|
|
31
|
+
build_command: str = "" # the command line we ran, for messages
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class RunSpec:
|
|
36
|
+
"""How to actually invoke a built artifact: the argv prefix and extra env.
|
|
37
|
+
|
|
38
|
+
The execution model takes this and adds the per-case bits (stdin, argv,
|
|
39
|
+
files) before handing it to the process layer.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
argv: List[str]
|
|
43
|
+
env: Dict[str, str] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Adapter(ABC):
|
|
47
|
+
"""The small interface every language exposes (Section 8.3)."""
|
|
48
|
+
|
|
49
|
+
name: str = ""
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def build(self, source: str, config: dict, workdir: str) -> BuildResult:
|
|
53
|
+
"""Compile/assemble/link (or no-op for interpreted languages)."""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def run_spec(self, build: BuildResult, config: dict) -> RunSpec:
|
|
57
|
+
"""The command + environment needed to run the built artifact."""
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def describe(self, config: dict) -> str:
|
|
61
|
+
"""One-line human summary, e.g. 'C, gcc, -std=gnu23, -O2'."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_REGISTRY: Dict[str, Adapter] = {}
|
|
65
|
+
|
|
66
|
+
# Map common file extensions to a language name, for auto-detection on import.
|
|
67
|
+
_EXTENSIONS = {
|
|
68
|
+
".c": "c",
|
|
69
|
+
".h": "c",
|
|
70
|
+
".cc": "cpp", ".cpp": "cpp", ".cxx": "cpp", ".c++": "cpp", ".hpp": "cpp",
|
|
71
|
+
".asm": "nasm", ".s": "nasm", ".nasm": "nasm",
|
|
72
|
+
".py": "python",
|
|
73
|
+
".java": "java",
|
|
74
|
+
".rs": "rust",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def register(adapter: Adapter) -> None:
|
|
79
|
+
_REGISTRY[adapter.name] = adapter
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_adapter(language: str) -> Adapter:
|
|
83
|
+
_load_builtins()
|
|
84
|
+
if language not in _REGISTRY:
|
|
85
|
+
from morvix.errors import UserError
|
|
86
|
+
|
|
87
|
+
known = ", ".join(sorted(_REGISTRY)) or "(none)"
|
|
88
|
+
raise UserError(
|
|
89
|
+
f"No adapter for language '{language}'.",
|
|
90
|
+
hint=f"Supported: {known}. Or use a raw build/run command (config --raw-build).",
|
|
91
|
+
)
|
|
92
|
+
return _REGISTRY[language]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def list_languages() -> List[str]:
|
|
96
|
+
_load_builtins()
|
|
97
|
+
return sorted(_REGISTRY)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def detect_language(path: str) -> Optional[str]:
|
|
101
|
+
"""Guess the language from a file's extension."""
|
|
102
|
+
import os
|
|
103
|
+
|
|
104
|
+
return _EXTENSIONS.get(os.path.splitext(path)[1].lower())
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
_loaded = False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _load_builtins() -> None:
|
|
111
|
+
# Import the concrete adapters once, so they self-register. Listed explicitly
|
|
112
|
+
# rather than auto-discovered to keep imports predictable.
|
|
113
|
+
global _loaded
|
|
114
|
+
if _loaded:
|
|
115
|
+
return
|
|
116
|
+
_loaded = True
|
|
117
|
+
from morvix.adapters import c, cpp, nasm, python, java, rust # noqa: F401
|
morvix/adapters/c.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# C language adapter.
|
|
2
|
+
#
|
|
3
|
+
# Compiles a single .c file with gcc (or a configured compiler) and produces
|
|
4
|
+
# a native binary. Config keys are all optional - sensible defaults apply so
|
|
5
|
+
# a plain {"language": "c"} config works out of the box.
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from morvix import process
|
|
10
|
+
from morvix.adapters import Adapter, BuildResult, RunSpec, register
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CAdapter(Adapter):
|
|
14
|
+
name = "c"
|
|
15
|
+
|
|
16
|
+
def build(self, source: str, config: dict, workdir: str) -> BuildResult:
|
|
17
|
+
compiler = config.get("compiler", "gcc")
|
|
18
|
+
process.require_tool(
|
|
19
|
+
compiler,
|
|
20
|
+
install_hint=f"Install gcc (e.g. 'sudo apt install gcc' or 'brew install gcc') "
|
|
21
|
+
f"and make sure '{compiler}' is on your PATH.",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
artifact = os.path.join(workdir, "a.out")
|
|
25
|
+
|
|
26
|
+
# Build up the compile command:
|
|
27
|
+
# - compiler, output flag, source
|
|
28
|
+
# - optional -std=, -O, extra flags, -I, -L, -l entries
|
|
29
|
+
cmd = [compiler, "-o", artifact, source]
|
|
30
|
+
|
|
31
|
+
std = config.get("std")
|
|
32
|
+
if std:
|
|
33
|
+
cmd.append(f"-std={std}")
|
|
34
|
+
|
|
35
|
+
opt = config.get("opt")
|
|
36
|
+
if opt:
|
|
37
|
+
cmd.append(f"-{opt}")
|
|
38
|
+
|
|
39
|
+
for flag in config.get("flags", []):
|
|
40
|
+
cmd.append(flag)
|
|
41
|
+
|
|
42
|
+
for inc in config.get("include", []):
|
|
43
|
+
cmd.extend(["-I", inc])
|
|
44
|
+
|
|
45
|
+
for libdir in config.get("libdirs", []):
|
|
46
|
+
cmd.extend(["-L", libdir])
|
|
47
|
+
|
|
48
|
+
for lib in config.get("lib", []):
|
|
49
|
+
cmd.append(f"-l{lib}")
|
|
50
|
+
|
|
51
|
+
res = process.run(cmd, wall_limit=180)
|
|
52
|
+
build_command = " ".join(cmd)
|
|
53
|
+
|
|
54
|
+
if not res.ok:
|
|
55
|
+
return BuildResult(
|
|
56
|
+
ok=False,
|
|
57
|
+
diagnostics=(res.stdout + res.stderr).decode("utf-8", "replace"),
|
|
58
|
+
error="compilation failed",
|
|
59
|
+
build_command=build_command,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return BuildResult(
|
|
63
|
+
ok=True,
|
|
64
|
+
artifact=artifact,
|
|
65
|
+
run_argv=[artifact],
|
|
66
|
+
run_env={},
|
|
67
|
+
build_command=build_command,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def run_spec(self, build: BuildResult, config: dict) -> RunSpec:
|
|
71
|
+
return RunSpec(argv=build.run_argv, env=build.run_env)
|
|
72
|
+
|
|
73
|
+
def describe(self, config: dict) -> str:
|
|
74
|
+
compiler = config.get("compiler", "gcc")
|
|
75
|
+
parts = ["C", compiler]
|
|
76
|
+
if "std" in config:
|
|
77
|
+
parts.append(f"-std={config['std']}")
|
|
78
|
+
if "opt" in config:
|
|
79
|
+
parts.append(f"-{config['opt']}")
|
|
80
|
+
for flag in config.get("flags", []):
|
|
81
|
+
parts.append(flag)
|
|
82
|
+
return ", ".join(parts)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
register(CAdapter())
|
morvix/adapters/cpp.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# C++ language adapter.
|
|
2
|
+
#
|
|
3
|
+
# Compiles a single .cpp file with g++ (or a configured compiler) and produces
|
|
4
|
+
# a native binary. Config keys are all optional - sensible defaults apply so
|
|
5
|
+
# a plain {"language": "cpp"} config works out of the box.
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from morvix import process
|
|
10
|
+
from morvix.adapters import Adapter, BuildResult, RunSpec, register
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CppAdapter(Adapter):
|
|
14
|
+
name = "cpp"
|
|
15
|
+
|
|
16
|
+
def build(self, source: str, config: dict, workdir: str) -> BuildResult:
|
|
17
|
+
compiler = config.get("compiler", "g++")
|
|
18
|
+
process.require_tool(
|
|
19
|
+
compiler,
|
|
20
|
+
install_hint=f"Install g++ (e.g. 'sudo apt install g++' or 'brew install gcc') "
|
|
21
|
+
f"and make sure '{compiler}' is on your PATH.",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
artifact = os.path.join(workdir, "a.out")
|
|
25
|
+
|
|
26
|
+
# Build up the compile command:
|
|
27
|
+
# - compiler, output flag, source
|
|
28
|
+
# - -std= (defaults to c++20), optional -O, extra flags, -I, -L, -l entries
|
|
29
|
+
cmd = [compiler, "-o", artifact, source]
|
|
30
|
+
|
|
31
|
+
std = config.get("std", "c++20")
|
|
32
|
+
cmd.append(f"-std={std}")
|
|
33
|
+
|
|
34
|
+
opt = config.get("opt")
|
|
35
|
+
if opt:
|
|
36
|
+
cmd.append(f"-{opt}")
|
|
37
|
+
|
|
38
|
+
for flag in config.get("flags", []):
|
|
39
|
+
cmd.append(flag)
|
|
40
|
+
|
|
41
|
+
for inc in config.get("include", []):
|
|
42
|
+
cmd.extend(["-I", inc])
|
|
43
|
+
|
|
44
|
+
for libdir in config.get("libdirs", []):
|
|
45
|
+
cmd.extend(["-L", libdir])
|
|
46
|
+
|
|
47
|
+
for lib in config.get("lib", []):
|
|
48
|
+
cmd.append(f"-l{lib}")
|
|
49
|
+
|
|
50
|
+
res = process.run(cmd, wall_limit=180)
|
|
51
|
+
build_command = " ".join(cmd)
|
|
52
|
+
|
|
53
|
+
if not res.ok:
|
|
54
|
+
return BuildResult(
|
|
55
|
+
ok=False,
|
|
56
|
+
diagnostics=(res.stdout + res.stderr).decode("utf-8", "replace"),
|
|
57
|
+
error="compilation failed",
|
|
58
|
+
build_command=build_command,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return BuildResult(
|
|
62
|
+
ok=True,
|
|
63
|
+
artifact=artifact,
|
|
64
|
+
run_argv=[artifact],
|
|
65
|
+
run_env={},
|
|
66
|
+
build_command=build_command,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def run_spec(self, build: BuildResult, config: dict) -> RunSpec:
|
|
70
|
+
return RunSpec(argv=build.run_argv, env=build.run_env)
|
|
71
|
+
|
|
72
|
+
def describe(self, config: dict) -> str:
|
|
73
|
+
compiler = config.get("compiler", "g++")
|
|
74
|
+
std = config.get("std", "c++20")
|
|
75
|
+
parts = ["C++", compiler, f"-std={std}"]
|
|
76
|
+
if "opt" in config:
|
|
77
|
+
parts.append(f"-{config['opt']}")
|
|
78
|
+
for flag in config.get("flags", []):
|
|
79
|
+
parts.append(flag)
|
|
80
|
+
return ", ".join(parts)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
register(CppAdapter())
|
morvix/adapters/java.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Java language adapter.
|
|
2
|
+
#
|
|
3
|
+
# Compiles a .java file with javac and runs the resulting class with java.
|
|
4
|
+
# The main class is inferred from the source file name and an optional package
|
|
5
|
+
# declaration. Config keys are all optional - a plain {"language": "java"}
|
|
6
|
+
# config works out of the box.
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from morvix import process
|
|
12
|
+
from morvix.adapters import Adapter, BuildResult, RunSpec, register
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Matches: package some.pkg.name;
|
|
16
|
+
_PACKAGE_RE = re.compile(r"^\s*package\s+([\w.]+)\s*;", re.MULTILINE)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _detect_main_class(source: str) -> str:
|
|
20
|
+
"""Return the fully-qualified main class name.
|
|
21
|
+
|
|
22
|
+
- The class name is always the file stem (filename without .java).
|
|
23
|
+
- Read the source text to look for a package declaration; if found,
|
|
24
|
+
prepend it with a dot separator.
|
|
25
|
+
"""
|
|
26
|
+
stem = os.path.splitext(os.path.basename(source))[0]
|
|
27
|
+
try:
|
|
28
|
+
text = open(source, encoding="utf-8", errors="replace").read()
|
|
29
|
+
except OSError:
|
|
30
|
+
return stem
|
|
31
|
+
m = _PACKAGE_RE.search(text)
|
|
32
|
+
if m:
|
|
33
|
+
return m.group(1) + "." + stem
|
|
34
|
+
return stem
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class JavaAdapter(Adapter):
|
|
38
|
+
name = "java"
|
|
39
|
+
|
|
40
|
+
def build(self, source: str, config: dict, workdir: str) -> BuildResult:
|
|
41
|
+
javac = config.get("javac", "javac")
|
|
42
|
+
java = config.get("java", "java")
|
|
43
|
+
classpath = config.get("classpath", "")
|
|
44
|
+
release = config.get("release")
|
|
45
|
+
|
|
46
|
+
# Raise a clear error if the JDK is not installed.
|
|
47
|
+
process.require_tool(
|
|
48
|
+
javac,
|
|
49
|
+
install_hint="Install a JDK (e.g. 'sudo apt install default-jdk' or "
|
|
50
|
+
"'brew install openjdk') and make sure 'javac' is on your PATH.",
|
|
51
|
+
)
|
|
52
|
+
process.require_tool(
|
|
53
|
+
java,
|
|
54
|
+
install_hint="Install a JRE/JDK and make sure 'java' is on your PATH.",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Build the javac command:
|
|
58
|
+
# - "-d workdir" places .class files under workdir
|
|
59
|
+
# - "--release N" pins the language level if configured
|
|
60
|
+
# - "-cp classpath" adds the extra classpath for compilation
|
|
61
|
+
# - "-sourcepath" lets javac pull in other .java files the source needs
|
|
62
|
+
source_dir = os.path.dirname(source) or "."
|
|
63
|
+
cmd = [javac, "-d", workdir]
|
|
64
|
+
if release:
|
|
65
|
+
cmd += ["--release", str(release)]
|
|
66
|
+
if classpath:
|
|
67
|
+
cmd += ["-cp", classpath]
|
|
68
|
+
cmd += ["-sourcepath", source_dir, source]
|
|
69
|
+
|
|
70
|
+
res = process.run(cmd, wall_limit=180)
|
|
71
|
+
build_command = " ".join(cmd)
|
|
72
|
+
|
|
73
|
+
if not res.ok:
|
|
74
|
+
return BuildResult(
|
|
75
|
+
ok=False,
|
|
76
|
+
diagnostics=(res.stdout + res.stderr).decode("utf-8", "replace"),
|
|
77
|
+
error="javac reported errors",
|
|
78
|
+
build_command=build_command,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
main_class = _detect_main_class(source)
|
|
82
|
+
|
|
83
|
+
# Runtime classpath = workdir (where .class files live) + extra classpath.
|
|
84
|
+
rt_cp = workdir + (os.pathsep + classpath if classpath else "")
|
|
85
|
+
run_argv = [java, "-cp", rt_cp, main_class]
|
|
86
|
+
|
|
87
|
+
return BuildResult(
|
|
88
|
+
ok=True,
|
|
89
|
+
artifact=workdir,
|
|
90
|
+
run_argv=run_argv,
|
|
91
|
+
run_env={},
|
|
92
|
+
build_command=build_command,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def run_spec(self, build: BuildResult, config: dict) -> RunSpec:
|
|
96
|
+
return RunSpec(argv=build.run_argv, env=build.run_env)
|
|
97
|
+
|
|
98
|
+
def describe(self, config: dict) -> str:
|
|
99
|
+
javac = config.get("javac", "javac")
|
|
100
|
+
java = config.get("java", "java")
|
|
101
|
+
parts = ["Java", javac, java]
|
|
102
|
+
release = config.get("release")
|
|
103
|
+
if release:
|
|
104
|
+
parts.append(f"--release {release}")
|
|
105
|
+
return ", ".join(parts)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
register(JavaAdapter())
|
morvix/adapters/nasm.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# NASM assembler adapter.
|
|
2
|
+
#
|
|
3
|
+
# Two-step build: assemble with nasm, then link with gcc (or ld).
|
|
4
|
+
# macOS (macho64 + ld for pure-syscall programs) is best-effort; the ABI
|
|
5
|
+
# and system call conventions differ from Linux elf64 in ways morvix does
|
|
6
|
+
# not abstract.
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from morvix.adapters import Adapter, BuildResult, RunSpec, register
|
|
12
|
+
from morvix import process
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _default_format():
|
|
16
|
+
return "macho64" if sys.platform == "darwin" else "elf64"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NasmAdapter(Adapter):
|
|
20
|
+
name = "nasm"
|
|
21
|
+
|
|
22
|
+
def build(self, source: str, config: dict, workdir: str) -> BuildResult:
|
|
23
|
+
assembler = config.get("assembler", "nasm")
|
|
24
|
+
fmt = config.get("format", _default_format())
|
|
25
|
+
linker = config.get("link", "gcc")
|
|
26
|
+
extra_flags = config.get("flags", [])
|
|
27
|
+
link_flags = config.get("linkflags", [])
|
|
28
|
+
|
|
29
|
+
# Locate required tools before we attempt anything.
|
|
30
|
+
asm_path = process.require_tool(
|
|
31
|
+
assembler,
|
|
32
|
+
f"Install nasm (e.g. 'brew install nasm' or 'apt install nasm').",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Step 1: assemble source -> object file.
|
|
36
|
+
obj = os.path.join(workdir, "obj.o")
|
|
37
|
+
asm_cmd = [asm_path, "-f", fmt, *extra_flags, source, "-o", obj]
|
|
38
|
+
res = process.run(asm_cmd, wall_limit=180)
|
|
39
|
+
if not res.ok:
|
|
40
|
+
diag = (res.stdout + res.stderr).decode("utf-8", "replace")
|
|
41
|
+
return BuildResult(
|
|
42
|
+
ok=False,
|
|
43
|
+
diagnostics=diag,
|
|
44
|
+
error="assembly failed",
|
|
45
|
+
build_command=" ".join(asm_cmd),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Step 2: link object -> executable.
|
|
49
|
+
out = os.path.join(workdir, "a.out")
|
|
50
|
+
if linker == "ld":
|
|
51
|
+
ld_path = process.require_tool(
|
|
52
|
+
"ld",
|
|
53
|
+
"Install ld (usually part of binutils or the system toolchain).",
|
|
54
|
+
)
|
|
55
|
+
link_cmd = [ld_path, obj, "-o", out, *link_flags]
|
|
56
|
+
else:
|
|
57
|
+
# Prefer gcc; fall back to cc.
|
|
58
|
+
cc = process.find_tool("gcc") or process.find_tool("cc")
|
|
59
|
+
if cc is None:
|
|
60
|
+
process.require_tool(
|
|
61
|
+
"gcc",
|
|
62
|
+
"Install gcc or cc (e.g. 'apt install gcc' or Xcode command-line tools).",
|
|
63
|
+
)
|
|
64
|
+
link_cmd = [cc, obj, "-o", out, *link_flags]
|
|
65
|
+
|
|
66
|
+
res2 = process.run(link_cmd, wall_limit=180)
|
|
67
|
+
if not res2.ok:
|
|
68
|
+
diag = (res2.stdout + res2.stderr).decode("utf-8", "replace")
|
|
69
|
+
return BuildResult(
|
|
70
|
+
ok=False,
|
|
71
|
+
diagnostics=diag,
|
|
72
|
+
error="link failed",
|
|
73
|
+
build_command=" ".join(link_cmd),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return BuildResult(
|
|
77
|
+
ok=True,
|
|
78
|
+
artifact=out,
|
|
79
|
+
run_argv=[out],
|
|
80
|
+
run_env={},
|
|
81
|
+
build_command=" ".join(asm_cmd) + " && " + " ".join(link_cmd),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def run_spec(self, build: BuildResult, config: dict) -> RunSpec:
|
|
85
|
+
return RunSpec(argv=build.run_argv, env=build.run_env)
|
|
86
|
+
|
|
87
|
+
def describe(self, config: dict) -> str:
|
|
88
|
+
assembler = config.get("assembler", "nasm")
|
|
89
|
+
fmt = config.get("format", _default_format())
|
|
90
|
+
linker = config.get("link", "gcc")
|
|
91
|
+
return f"NASM, {assembler}, {fmt}, link={linker}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
register(NasmAdapter())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Python language adapter.
|
|
2
|
+
#
|
|
3
|
+
# Python scripts need no compilation step, so build() is a lightweight
|
|
4
|
+
# syntax check via "python3 -m py_compile". If the check passes the script
|
|
5
|
+
# itself is the artifact and run_argv points straight at the interpreter.
|
|
6
|
+
|
|
7
|
+
from morvix import process
|
|
8
|
+
from morvix.adapters import Adapter, BuildResult, RunSpec, register
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PythonAdapter(Adapter):
|
|
12
|
+
name = "python"
|
|
13
|
+
|
|
14
|
+
def build(self, source: str, config: dict, workdir: str) -> BuildResult:
|
|
15
|
+
interpreter = config.get("interpreter") or "python3"
|
|
16
|
+
|
|
17
|
+
# Ensure the interpreter exists before doing anything else.
|
|
18
|
+
process.require_tool(
|
|
19
|
+
interpreter,
|
|
20
|
+
install_hint=f"Install Python and make sure '{interpreter}' is on your PATH.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Run an optional syntax check; a real compile error should surface
|
|
24
|
+
# here rather than at run time so the user gets a clear message.
|
|
25
|
+
cmd = [interpreter, "-m", "py_compile", source]
|
|
26
|
+
res = process.run(cmd, wall_limit=30)
|
|
27
|
+
if not res.ok:
|
|
28
|
+
diagnostics = (res.stdout + res.stderr).decode("utf-8", "replace")
|
|
29
|
+
return BuildResult(
|
|
30
|
+
ok=False,
|
|
31
|
+
diagnostics=diagnostics,
|
|
32
|
+
error="syntax error",
|
|
33
|
+
build_command=" ".join(cmd),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# No binary to produce - the script is the artifact.
|
|
37
|
+
return BuildResult(
|
|
38
|
+
ok=True,
|
|
39
|
+
artifact=source,
|
|
40
|
+
run_argv=[interpreter, source],
|
|
41
|
+
run_env={},
|
|
42
|
+
build_command=" ".join(cmd),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def run_spec(self, build: BuildResult, config: dict) -> RunSpec:
|
|
46
|
+
return RunSpec(argv=build.run_argv, env=build.run_env)
|
|
47
|
+
|
|
48
|
+
def describe(self, config: dict) -> str:
|
|
49
|
+
interpreter = config.get("interpreter") or "python3"
|
|
50
|
+
return f"Python, {interpreter}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
register(PythonAdapter())
|
morvix/adapters/rust.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Rust language adapter.
|
|
2
|
+
#
|
|
3
|
+
# Compiles a single .rs file with rustc and produces a native binary.
|
|
4
|
+
# Cargo-project builds are a future extension; single-file rustc is supported now.
|
|
5
|
+
# Config keys are all optional - sensible defaults apply.
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from morvix import process
|
|
10
|
+
from morvix.adapters import Adapter, BuildResult, RunSpec, register
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RustAdapter(Adapter):
|
|
14
|
+
name = "rust"
|
|
15
|
+
|
|
16
|
+
def build(self, source: str, config: dict, workdir: str) -> BuildResult:
|
|
17
|
+
rustc = config.get("rustc", "rustc")
|
|
18
|
+
process.require_tool(
|
|
19
|
+
rustc,
|
|
20
|
+
"Install Rust via rustup for the rust adapter.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
artifact = os.path.join(workdir, "a.out")
|
|
24
|
+
|
|
25
|
+
# Build up the compile command:
|
|
26
|
+
# - rustc, optional --edition, optional -O, source, -o output
|
|
27
|
+
cmd = [rustc]
|
|
28
|
+
|
|
29
|
+
edition = config.get("edition")
|
|
30
|
+
if edition:
|
|
31
|
+
cmd.extend(["--edition", str(edition)])
|
|
32
|
+
|
|
33
|
+
if config.get("release"):
|
|
34
|
+
cmd.append("-O")
|
|
35
|
+
|
|
36
|
+
cmd.extend([source, "-o", artifact])
|
|
37
|
+
|
|
38
|
+
res = process.run(cmd, wall_limit=180)
|
|
39
|
+
build_command = " ".join(cmd)
|
|
40
|
+
|
|
41
|
+
if not res.ok:
|
|
42
|
+
return BuildResult(
|
|
43
|
+
ok=False,
|
|
44
|
+
diagnostics=(res.stdout + res.stderr).decode("utf-8", "replace"),
|
|
45
|
+
error="compilation failed",
|
|
46
|
+
build_command=build_command,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return BuildResult(
|
|
50
|
+
ok=True,
|
|
51
|
+
artifact=artifact,
|
|
52
|
+
run_argv=[artifact],
|
|
53
|
+
run_env={},
|
|
54
|
+
build_command=build_command,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def run_spec(self, build: BuildResult, config: dict) -> RunSpec:
|
|
58
|
+
return RunSpec(argv=build.run_argv, env=build.run_env)
|
|
59
|
+
|
|
60
|
+
def describe(self, config: dict) -> str:
|
|
61
|
+
rustc = config.get("rustc", "rustc")
|
|
62
|
+
parts = ["Rust", rustc]
|
|
63
|
+
if "edition" in config:
|
|
64
|
+
parts.append(f"--edition {config['edition']}")
|
|
65
|
+
if config.get("release"):
|
|
66
|
+
parts.append("-O")
|
|
67
|
+
return ", ".join(parts)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
register(RustAdapter())
|