diff --git a/mwccgap/compiler.py b/mwccgap/compiler.py index 1415b4a..d936d38 100644 --- a/mwccgap/compiler.py +++ b/mwccgap/compiler.py @@ -1,13 +1,17 @@ import os +import re import subprocess import sys import tempfile from pathlib import Path from typing import List, Optional +from .makerule import MakeRule class Compiler: + obj_bytes: bytes | None + make_rule: MakeRule | None def __init__( self, @@ -23,6 +27,17 @@ def __init__( self.mwcc_path = mwcc_path self.use_wibo = use_wibo self.wibo_path = wibo_path + self.obj_bytes = None + self.make_rule = None + + # gcc compatibility may be enabled and then + # disabled by a later flag + self.gcc_deps = False + for flag in self.c_flags: + if flag in ["-gccdep", "-gccdepends"]: + self.gcc_deps = True + if flag in ["-nogccdep", "-nogccdepends"]: + self.gcc_deps = False def _compile_file( self, @@ -57,7 +72,8 @@ def compile_file( c_file: Path, ) -> bytes: with tempfile.TemporaryDirectory() as temp_dir: - o_file = Path(temp_dir) / "result.o" + temp_path = Path(temp_dir) + o_file = temp_path / "result.o" stdout, stderr = self._compile_file( c_file, o_file, @@ -71,8 +87,29 @@ def compile_file( if not o_file.is_file(): raise Exception(f"Error compiling {c_file}") - obj_bytes = o_file.read_bytes() - if len(obj_bytes) == 0: + self.obj_bytes = o_file.read_bytes() + if len(self.obj_bytes) == 0: raise Exception(f"Error compiling {c_file}, object is empty") - return obj_bytes + self._handle_dependency_file(c_file, temp_path) + + return self.obj_bytes + + # the compiler may emit a dependency file in addition to the object + # file. if so, we want to make those bytes available to the caller + def _handle_dependency_file(self, c_file: Path, temp_dir: Path): + self.make_rule = None + + if self.gcc_deps: + d_file = Path(temp_dir) / "result.d" + if d_file.is_file(): + dep_bytes = d_file.read_bytes() + self.make_rule = MakeRule(dep_bytes, self.use_wibo) + elif "-MD" in self.c_flags or "-MMD" in self.c_flags: + # in MetroWerks mode, the dependency file will be put in cwd + # with the same name as the source file but with a .d extension + d_file = Path(re.sub("\\.c$", ".d", c_file.name)) + if d_file.is_file(): + dep_bytes = d_file.read_bytes() + d_file.unlink() + self.make_rule = MakeRule(dep_bytes, self.use_wibo) diff --git a/mwccgap/makerule.py b/mwccgap/makerule.py new file mode 100644 index 0000000..8d43c2f --- /dev/null +++ b/mwccgap/makerule.py @@ -0,0 +1,70 @@ +from pathlib import Path +import re + + +class MakeRule: + target: str + source: str | None + includes: list[str] + + def __init__(self, data: bytes, use_wibo=False): + if use_wibo: + encoding = "iso-8859-1" + else: + encoding = "utf-8" + + rule = data.decode(encoding) + rule = re.sub(r"\\[\r\n]+", " ", rule) + (target, remaining) = re.split(": ", rule) + files = remaining.split() + files.insert(0, target) + + if use_wibo: + files = [path_from_wibo(p) for p in files] + + # the first file is the target, the second is the + self.target = files.pop(0) + self.source = files.pop(0) + self.includes = files + + def as_str(self): + rule = f"{self.target}: " + if self.source is not None: + rule += f"{self.source} " + for file in self.includes: + rule += f"\\\n\t{file} " + rule += "\n" + + return rule + + +# an implementation of the wibo translation from "windows" +# path to a unix path +def path_from_wibo(path_str: str) -> Path: + path_str = path_str.replace("\\", "/") + + # remove the extended path prefix + if path_str.startswith("//?/"): + path_str = path_str[4:] + + # remove the drive letter + if path_str.lower().startswith("z:/"): + path_str = path_str[2:] + + # if it exists, we're done + path = Path(path_str) + if path.is_file(): + return path + + # otherwise try to find a case insensitive match + new_path = Path(".") + for part in path.parts: + candidate = new_path / part + if new_path.is_dir(): + for entry in new_path.iterdir(): + if entry.name.lower() == part.lower(): + candidate = new_path / entry.name + break + new_path = candidate + + return new_path diff --git a/mwccgap/mwccgap.py b/mwccgap/mwccgap.py index 53576f9..180c8dc 100755 --- a/mwccgap/mwccgap.py +++ b/mwccgap/mwccgap.py @@ -1,4 +1,5 @@ import copy +import sys import tempfile from pathlib import Path @@ -14,6 +15,7 @@ ) from .elf import Elf, TextSection, Relocation from .preprocessor import Preprocessor +from .makerule import MakeRule def process_c_file( @@ -42,6 +44,22 @@ def process_c_file( else: obj_bytes = compiler.compile_file(c_file) + if compiler.make_rule is not None: + rule = compiler.make_rule + if sys.stdin.isatty(): + rule.source = str(c_file) + else: + rule.source = None + rule.target = str(o_file) + # in gcc mode, the dependency is writeen near the target + if compiler.gcc_deps: + d_file = o_file.with_suffix(f"{o_file.suffix}.d") + # in mw mode, it's written in cwd, if this is a tempfile, this output will not make sense + else: + d_file = c_file.with_suffix(".d") + d_file.parent.mkdir(exist_ok=True, parents=True) + d_file.write_bytes(rule.as_str().encode("utf-8")) + precompiled_elf = Elf(obj_bytes) # for now we only care about the names of the functions that exist c_functions = set(f.function_name for f in precompiled_elf.get_functions()) diff --git a/tests/test_deps.py b/tests/test_deps.py new file mode 100644 index 0000000..872e587 --- /dev/null +++ b/tests/test_deps.py @@ -0,0 +1,140 @@ +import tempfile +import unittest +import os +import re +import shutil + +from pathlib import Path +from mwccgap.compiler import Compiler +from mwccgap.makerule import MakeRule + +mwcc = os.getenv("MWCC") +if mwcc is None: + mwcc = "mwccpsp.exe" + + +def has_wibo_and_mwcc(): + wibo = shutil.which("wibo") + mwcc_exe = shutil.which(mwcc) + return wibo is not None and mwcc_exe is not None + + +class TestDependencies(unittest.TestCase): + def __init__(self, x): + super().__init__(x) + self.wibo = shutil.which("wibo") + self.mwcc = shutil.which(mwcc) + + def has_dependencies(self): + return self.wibo is not None and self.mwcc is not None + + def _compile(self, c_flags: list[str], program: str): + compiler = Compiler(c_flags, self.mwcc, True, self.wibo) + + test_path = os.path.abspath(__file__) + test_dir = os.path.dirname(test_path) + with tempfile.NamedTemporaryFile(suffix=".c", dir=test_dir) as c_file: + c_file.write(program.encode("utf-8")) + c_file.flush() + compiler.compile_file(Path(c_file.name)) + + return compiler + + @unittest.skipUnless(has_wibo_and_mwcc(), "requires wibo and mwcc") + def test_dependencies_gcc_behavior(self): + compiler = self._compile( + ["-MD", "-gccdep"], + """ +int add(int a, int b) { + return a + b; +} +""", + ) + + rule = compiler.make_rule + + self.assertTrue( + str(rule.target).startswith("/tmp"), + f"target: {rule.target} should start with /tmp", + ) + self.assertTrue( + str(rule.target).endswith("result.o"), + f"target: {rule.target} should end with result.o", + ) + self.assertTrue(str(rule.source).endswith(".c")) + self.assertEqual(0, len(rule.includes)) + + @unittest.skipUnless(has_wibo_and_mwcc(), "requires wibo and mwcc") + def test_no_dependencies_mw_behavior(self): + compiler = self._compile( + ["-MD"], + """int add(int a, int b) { + return a + b; +} +""", + ) + + rule = compiler.make_rule + + self.assertTrue( + str(rule.target).startswith("/tmp"), + f"target: {rule.target} should start with /tmp", + ) + self.assertTrue( + str(rule.target).endswith("result.o"), + f"target: {rule.target} should end with result.o", + ) + self.assertTrue(str(rule.source).endswith(".c")) + self.assertEqual(0, len(rule.includes)) + + @unittest.skipUnless(has_wibo_and_mwcc(), "requires wibo and mwcc") + def test_no_depencies(self): + compiler = self._compile( + [], + """int add(int a, int b) { + return a + b; +} +""", + ) + + rule = compiler.make_rule + + self.assertIsNone(rule) + + def test_make_rule_simple(self): + wibo_make_rule = "Z:\\tmp\\tmpfmuzt8mz\\result.o: test.c \r\n".encode("ascii") + + rule = MakeRule(wibo_make_rule, True) + + self.assertEqual(Path("/tmp/tmpfmuzt8mz/result.o"), rule.target) + self.assertEqual(Path("test.c"), rule.source) + self.assertEqual([], rule.includes) + self.assertEqual("/tmp/tmpfmuzt8mz/result.o: test.c \n", rule.as_str()) + + def test_make_rule_with_includes(self): + wibo_make_rule = ( + "Z:\\tmp\\tmpfkcxmvnu\\result.o: test2.c \\\r\n" + "\tZ:\\home\\user\\Projects\\mwccgap\\decl.h \\\r\n" + "\t\\\\?\\Z:\\home\\user\\Projects\\mwccgap\\lib.h \r\n" + ).encode("ascii") + + rule = MakeRule(wibo_make_rule, True) + + expected_rule = ( + "/tmp/tmpfkcxmvnu/result.o: test2.c \\\n" + "\t/home/user/Projects/mwccgap/decl.h \\\n" + "\t/home/user/Projects/mwccgap/lib.h \n" + ) + self.assertEqual(expected_rule, rule.as_str()) + + def test_unix_deps(self): + make_rule = ( + "/tmp/tmpfkcxmvnu/result.o: test.c \\\n" "\tdecl.h \\\n" "\tlib.h \n" + ).encode("utf-8") + + rule = MakeRule(make_rule, False) + + self.assertEqual("/tmp/tmpfkcxmvnu/result.o", rule.target) + self.assertEqual("test.c", rule.source) + self.assertEqual(["decl.h", "lib.h"], rule.includes) + self.assertEqual(make_rule.decode("utf-8"), rule.as_str())