Files

320 lines
10 KiB
Python
Raw Permalink Normal View History

2021-07-07 00:48:01 +09:00
import logging
import os
2022-04-26 03:46:08 -04:00
import re
2021-07-07 00:48:01 +09:00
import subprocess
2021-10-11 16:59:33 +01:00
from dataclasses import dataclass
2022-10-28 22:17:07 -04:00
import time
2022-07-03 10:43:59 -04:00
2023-09-30 07:54:40 +02:00
from typing import (
Any,
Callable,
Dict,
Optional,
Tuple,
TYPE_CHECKING,
TypeVar,
Sequence,
)
2022-03-13 13:33:19 -04:00
from django.conf import settings
from coreapp import compilers, platforms
from coreapp.compilers import Compiler
from coreapp.flags import Language
2022-03-13 13:33:19 -04:00
from coreapp.platforms import Platform
import coreapp.util as util
2021-07-07 00:48:01 +09:00
2022-02-17 18:47:04 +00:00
from .error import AssemblyError, CompilationError
2023-10-27 23:40:59 +09:00
from .libraries import Library
2022-02-17 18:47:04 +00:00
from .models.scratch import Asm, Assembly
from .sandbox import Sandbox
# Thanks to Guido van Rossum for the following fix
# https://github.com/python/mypy/issues/5107#issuecomment-529372406
if TYPE_CHECKING:
F = TypeVar("F")
def lru_cache(maxsize: int = 128, typed: bool = False) -> Callable[[F], F]:
pass
else:
from functools import lru_cache
2021-07-07 00:48:01 +09:00
logger = logging.getLogger(__name__)
2021-08-26 14:49:33 -04:00
PATH: str
2021-08-26 13:56:05 +01:00
if settings.USE_SANDBOX_JAIL:
2021-08-26 14:49:33 -04:00
PATH = "/bin:/usr/bin"
2021-08-26 13:56:05 +01:00
else:
2021-08-26 14:49:33 -04:00
PATH = os.environ["PATH"]
2021-08-26 13:52:12 +01:00
2023-10-27 23:40:59 +09:00
WINE = "wine"
WIBO = "wibo"
2022-02-20 09:21:38 -05:00
2023-06-03 16:37:03 +01:00
@dataclass
class DiffResult:
result: Optional[Dict[str, Any]] = None
errors: Optional[str] = None
2023-06-03 16:37:03 +01:00
2021-11-19 13:51:11 -05:00
@dataclass
class CompilationResult:
elf_object: bytes
errors: str
2022-02-20 09:21:38 -05:00
2021-08-26 14:49:33 -04:00
def _check_assembly_cache(*args: str) -> Tuple[Optional[Assembly], str]:
2021-08-08 23:58:01 +09:00
hash = util.gen_hash(args)
2021-08-09 22:57:26 +09:00
return Assembly.objects.filter(hash=hash).first(), hash
2021-08-08 23:58:01 +09:00
2021-08-25 13:23:50 +02:00
2021-07-07 00:48:01 +09:00
class CompilerWrapper:
2021-08-26 14:49:33 -04:00
@staticmethod
2022-03-13 13:33:19 -04:00
def filter_compiler_flags(compiler_flags: str) -> str:
2021-08-25 13:23:50 +02:00
# Remove irrelevant flags that are part of the base compiler configs or
# don't affect matching, but clutter the compiler settings field.
# TODO: use cfg for this?
2021-08-25 13:23:50 +02:00
skip_flags_with_args = {
"-B",
"-I",
"-U",
}
2021-08-25 13:23:50 +02:00
skip_flags = {
"-ffreestanding",
"-non_shared",
2021-08-25 13:23:50 +02:00
"-Xcpluscomm",
"-Wab,-r4300_mul",
"-c",
}
2022-02-03 12:21:26 -05:00
2021-08-25 13:23:50 +02:00
skip_next = False
flags = []
2021-10-26 08:14:48 -04:00
for flag in compiler_flags.split():
2021-08-25 13:23:50 +02:00
if skip_next:
skip_next = False
continue
if flag in skip_flags:
continue
if flag in skip_flags_with_args:
skip_next = True
continue
if any(flag.startswith(f) for f in skip_flags_with_args):
continue
flags.append(flag)
return " ".join(flags)
@staticmethod
def filter_compile_errors(input: str) -> str:
2022-04-26 03:46:08 -04:00
filter_strings = [
2022-07-29 11:41:20 -04:00
r"wine: could not load .*\.dll.*\n?",
r"wineserver: could not save registry .*\n?",
r"### .*\.exe Driver Error:.*\n?",
r"# Cannot find my executable .*\n?",
r"### MWCPPC\.exe Driver Error:.*\n?",
2025-02-10 10:01:41 +00:00
r"Fontconfig error:.*\n?",
2022-04-26 03:46:08 -04:00
]
for str in filter_strings:
input = re.sub(str, "", input)
return input.strip()
2021-07-07 00:48:01 +09:00
@staticmethod
@lru_cache(maxsize=settings.COMPILATION_CACHE_SIZE)
2022-02-20 09:21:38 -05:00
def compile_code(
compiler: Compiler,
compiler_flags: str,
code: str,
context: str,
function: str = "",
2023-09-30 07:54:40 +02:00
libraries: Sequence[Library] = (),
2022-02-20 09:21:38 -05:00
) -> CompilationResult:
2022-03-13 13:33:19 -04:00
if compiler == compilers.DUMMY:
return CompilationResult(f"compiled({context}\n{code}".encode("UTF-8"), "")
2021-09-06 07:21:51 +09:00
code = code.replace("\r\n", "\n")
context = context.replace("\r\n", "\n")
2021-08-09 23:35:34 +09:00
2021-08-11 13:37:24 -04:00
with Sandbox() as sandbox:
ext = compiler.language.get_file_extension()
code_file = f"code.{ext}"
2023-08-30 18:27:06 +01:00
src_file = f"src.{ext}"
ctx_file = f"ctx.{ext}"
code_path = sandbox.path / code_file
2021-08-10 22:44:39 -04:00
object_path = sandbox.path / "object.o"
skip_line_directive = (
compiler.is_ido and compiler.language == Language.PASCAL
)
with code_path.open("w") as f:
if not skip_line_directive:
f.write(f'#line 1 "{ctx_file}"\n')
f.write(context)
2022-02-20 09:21:38 -05:00
f.write("\n")
2021-07-07 01:54:41 +09:00
if not skip_line_directive:
f.write(f'#line 1 "{src_file}"\n')
f.write(code)
2022-02-20 09:21:38 -05:00
f.write("\n")
2022-03-13 13:33:19 -04:00
cc_cmd = compiler.cc
2022-02-03 12:21:26 -05:00
2023-08-30 18:27:06 +01:00
# MWCC requires the file to exist for DWARF line numbers,
# and requires the file contents for error messages
2022-07-15 14:47:13 +01:00
if compiler.is_mwcc:
ctx_path = sandbox.path / ctx_file
2022-07-15 14:47:13 +01:00
ctx_path.touch()
2023-08-30 18:27:06 +01:00
with ctx_path.open("w") as f:
f.write(context)
f.write("\n")
src_path = sandbox.path / src_file
src_path.touch()
with src_path.open("w") as f:
f.write(code)
f.write("\n")
2022-07-15 14:47:13 +01:00
2022-02-03 12:21:26 -05:00
# IDO hack to support -KPIC
2022-03-13 13:33:19 -04:00
if compiler.is_ido and "-KPIC" in compiler_flags:
2022-02-03 12:21:26 -05:00
cc_cmd = cc_cmd.replace("-non_shared", "")
if compiler.platform != platforms.DUMMY and not compiler.path.exists():
logging.warning("%s does not exist, creating it!", compiler.path)
compiler.path.mkdir(parents=True)
# Run compiler
2021-08-10 22:44:39 -04:00
try:
2022-10-28 22:17:07 -04:00
st = round(time.time() * 1000)
2023-09-30 07:54:40 +02:00
libraries_compiler_flags = " ".join(
(
compiler.library_include_flag
+ str(lib.get_include_path(compiler.platform.id))
2023-09-30 07:54:40 +02:00
for lib in libraries
)
)
compile_proc = sandbox.run_subprocess(
2022-02-03 12:21:26 -05:00
cc_cmd,
mounts=(
[compiler.path] if compiler.platform != platforms.DUMMY else []
),
2021-08-10 22:44:39 -04:00
shell=True,
env={
2022-02-20 09:21:38 -05:00
"PATH": PATH,
2022-07-29 11:41:20 -04:00
"WINE": WINE,
"WIBO": WIBO,
2022-02-20 09:21:38 -05:00
"INPUT": sandbox.rewrite_path(code_path),
"OUTPUT": sandbox.rewrite_path(object_path),
2022-03-13 13:33:19 -04:00
"COMPILER_DIR": sandbox.rewrite_path(compiler.path),
2023-09-30 07:54:40 +02:00
"COMPILER_FLAGS": sandbox.quote_options(
compiler_flags + " " + libraries_compiler_flags
),
"FUNCTION": function,
2022-02-20 09:21:38 -05:00
"MWCIncludes": "/tmp",
2022-07-29 07:44:16 +01:00
"TMPDIR": "/tmp",
2022-02-20 09:21:38 -05:00
},
timeout=settings.COMPILATION_TIMEOUT_SECONDS,
2022-02-20 09:21:38 -05:00
)
2022-10-28 22:17:07 -04:00
et = round(time.time() * 1000)
logging.debug(f"Compilation finished in: {et - st} ms")
2021-08-10 22:44:39 -04:00
except subprocess.CalledProcessError as e:
# Compilation failed
msg = e.stdout
2022-02-21 17:19:54 +00:00
logging.debug("Compilation failed: %s", msg)
raise CompilationError(CompilerWrapper.filter_compile_errors(msg))
2022-04-26 03:46:08 -04:00
except ValueError as e:
# Shlex issue?
logging.debug("Compilation failed: %s", e)
raise CompilationError(str(e))
except subprocess.TimeoutExpired as e:
raise CompilationError("Compilation failed: timeout expired")
2021-07-07 00:48:01 +09:00
2021-08-10 22:44:39 -04:00
if not object_path.exists():
error_msg = (
"Compiler did not create an object file: %s" % compile_proc.stdout
)
logging.debug(error_msg)
raise CompilationError(error_msg)
object_bytes = object_path.read_bytes()
if not object_bytes:
raise CompilationError("Compiler created an empty object file")
compile_errors = CompilerWrapper.filter_compile_errors(compile_proc.stdout)
2024-02-07 19:46:44 +01:00
return CompilationResult(object_bytes, compile_errors)
2021-07-29 23:03:04 +09:00
@staticmethod
2022-03-13 13:33:19 -04:00
def assemble_asm(platform: Platform, asm: Asm) -> Assembly:
if not platform.assemble_cmd:
raise AssemblyError(
f"Assemble command for platform {platform.id} not found"
)
2021-10-11 16:59:33 +01:00
2022-03-13 13:33:19 -04:00
cached_assembly, hash = _check_assembly_cache(platform.id, asm.hash)
if cached_assembly:
logger.debug(f"Assembly cache hit! hash: {hash}")
return cached_assembly
2021-08-08 23:58:01 +09:00
2022-03-13 13:33:19 -04:00
if platform == platforms.DUMMY:
assembly = Assembly(
hash=hash,
2022-03-13 13:33:19 -04:00
arch=platform.arch,
source_asm=asm,
)
assembly.save()
return assembly
2021-08-11 13:37:24 -04:00
with Sandbox() as sandbox:
asm_prelude_path = sandbox.path / "prelude.s"
asm_prelude_path.write_text(platform.asm_prelude)
2021-08-10 22:44:39 -04:00
asm_path = sandbox.path / "asm.s"
2022-11-15 22:46:15 +00:00
data = asm.data.replace(".section .late_rodata", ".late_rodata")
asm_path.write_text(data + "\n")
2021-07-29 23:03:04 +09:00
2021-08-10 22:44:39 -04:00
object_path = sandbox.path / "object.o"
2021-08-10 00:00:02 +09:00
2021-08-10 22:44:39 -04:00
# Run assembler
try:
assemble_proc = sandbox.run_subprocess(
2022-03-13 13:33:19 -04:00
platform.assemble_cmd,
2021-08-23 20:58:18 +09:00
mounts=[],
2021-08-10 22:44:39 -04:00
shell=True,
env={
2022-02-20 09:21:38 -05:00
"PATH": PATH,
"PRELUDE": sandbox.rewrite_path(asm_prelude_path),
2022-02-20 09:21:38 -05:00
"INPUT": sandbox.rewrite_path(asm_path),
"OUTPUT": sandbox.rewrite_path(object_path),
2023-07-03 13:51:58 +01:00
"COMPILER_BASE_PATH": sandbox.rewrite_path(
settings.COMPILER_BASE_PATH
),
2022-02-20 09:21:38 -05:00
},
timeout=settings.ASSEMBLY_TIMEOUT_SECONDS,
2022-02-20 09:21:38 -05:00
)
2021-08-10 22:44:39 -04:00
except subprocess.CalledProcessError as e:
raise AssemblyError.from_process_error(e)
except subprocess.TimeoutExpired as e:
raise AssemblyError("Timeout expired")
2021-07-29 23:03:04 +09:00
# Assembly failed
2021-08-10 22:44:39 -04:00
if assemble_proc.returncode != 0:
2022-02-20 09:21:38 -05:00
raise AssemblyError(
f"Assembler failed with error code {assemble_proc.returncode}"
)
2021-08-10 22:44:39 -04:00
if not object_path.exists():
raise AssemblyError("Assembler did not create an object file")
2021-08-10 01:08:33 +09:00
assembly = Assembly(
hash=hash,
2022-03-13 13:33:19 -04:00
arch=platform.arch,
source_asm=asm,
elf_object=object_path.read_bytes(),
)
assembly.save()
return assembly