Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions mwccgap/compiler.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it simpler to make this b"" rather than None?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few cases that I didn't add support for yet, but was planning on coming back to for completeness. The -M, and -MM options as well as -make, -P, -S options don't emit object code and would not have obj_bytes. Some of those need similar post-processing. It would be fair to say mwccgap shouldn't be used for those cases, but I was considering it as a generalized mwcc wrapper rather than only for cases where ASM rewriting is necessary.

make_rule: MakeRule | None

def __init__(
self,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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)
70 changes: 70 additions & 0 deletions mwccgap/makerule.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions mwccgap/mwccgap.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import copy
import sys
import tempfile

from pathlib import Path
Expand All @@ -14,6 +15,7 @@
)
from .elf import Elf, TextSection, Relocation
from .preprocessor import Preprocessor
from .makerule import MakeRule


def process_c_file(
Expand Down Expand Up @@ -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())
Expand Down
140 changes: 140 additions & 0 deletions tests/test_deps.py
Original file line number Diff line number Diff line change
@@ -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())