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.
Files changed (73) hide show
  1. morvix/__init__.py +10 -0
  2. morvix/__main__.py +8 -0
  3. morvix/adapters/__init__.py +117 -0
  4. morvix/adapters/c.py +85 -0
  5. morvix/adapters/cpp.py +83 -0
  6. morvix/adapters/java.py +108 -0
  7. morvix/adapters/nasm.py +94 -0
  8. morvix/adapters/python.py +53 -0
  9. morvix/adapters/rust.py +70 -0
  10. morvix/app.py +75 -0
  11. morvix/banner.py +22 -0
  12. morvix/cases.py +129 -0
  13. morvix/commands/__init__.py +1 -0
  14. morvix/commands/bruteforce_cmd.py +45 -0
  15. morvix/commands/clean_cmd.py +53 -0
  16. morvix/commands/config_cmd.py +179 -0
  17. morvix/commands/exit_cmd.py +14 -0
  18. morvix/commands/gen_cmd.py +219 -0
  19. morvix/commands/help_cmd.py +99 -0
  20. morvix/commands/import_cmd.py +86 -0
  21. morvix/commands/init_cmd.py +111 -0
  22. morvix/commands/model_cmd.py +56 -0
  23. morvix/commands/open_cmd.py +69 -0
  24. morvix/commands/package_cmd.py +109 -0
  25. morvix/commands/reference_cmd.py +60 -0
  26. morvix/commands/result_cmd.py +228 -0
  27. morvix/commands/run_cmd.py +139 -0
  28. morvix/commands/runner_cmd.py +327 -0
  29. morvix/commands/status_cmd.py +75 -0
  30. morvix/commands/workflow_cmd.py +140 -0
  31. morvix/comparators.py +108 -0
  32. morvix/compare.py +110 -0
  33. morvix/components/__init__.py +1 -0
  34. morvix/components/choice.py +33 -0
  35. morvix/components/confirm.py +41 -0
  36. morvix/components/form.py +254 -0
  37. morvix/components/progress.py +45 -0
  38. morvix/components/selection.py +235 -0
  39. morvix/components/table.py +114 -0
  40. morvix/context.py +74 -0
  41. morvix/errors.py +45 -0
  42. morvix/execmodels/__init__.py +3 -0
  43. morvix/execmodels/args.py +36 -0
  44. morvix/execmodels/file.py +60 -0
  45. morvix/execmodels/interactive.py +157 -0
  46. morvix/execmodels/library.py +119 -0
  47. morvix/generators.py +313 -0
  48. morvix/help_text.py +178 -0
  49. morvix/judge.py +203 -0
  50. morvix/layout.py +48 -0
  51. morvix/manifest.py +95 -0
  52. morvix/messages.py +57 -0
  53. morvix/models.py +124 -0
  54. morvix/packaging.py +167 -0
  55. morvix/process.py +337 -0
  56. morvix/project.py +250 -0
  57. morvix/readme.py +151 -0
  58. morvix/registry.py +178 -0
  59. morvix/results.py +161 -0
  60. morvix/runner_build.py +82 -0
  61. morvix/runner_core/morvix_runner.py +1245 -0
  62. morvix/runner_core/run.sh +36 -0
  63. morvix/shapes.py +249 -0
  64. morvix/shell.py +196 -0
  65. morvix/suggestions.py +113 -0
  66. morvix/theme.py +75 -0
  67. morvix/version.py +7 -0
  68. morvix/workflow.py +89 -0
  69. morvix-0.1.0.dist-info/METADATA +126 -0
  70. morvix-0.1.0.dist-info/RECORD +73 -0
  71. morvix-0.1.0.dist-info/WHEEL +4 -0
  72. morvix-0.1.0.dist-info/entry_points.txt +2 -0
  73. 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,8 @@
1
+ # Lets you run the tool with `python -m morvix`.
2
+
3
+ import sys
4
+
5
+ from morvix.app import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -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())
@@ -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())
@@ -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())
@@ -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())