Files
PrimeAPI/script/BuildModule.py
T

563 lines
16 KiB
Python

from Stream import *
from Mangle import *
from DolFile import *
from PreplfFile import *
import glob
import os
import re
import subprocess
import sys
# Environment
primeApiRoot = os.path.realpath(os.path.dirname(os.path.realpath(__file__)) + "\\..")
dolphinRoot = ""
cwRoot = ""
cwToolsDir = ""
compilerPath = ""
linkerPath = ""
# Configuration
projDir = ""
buildDir = ""
moduleName = "Mod"
outFile = ""
dolFile = DolFile()
dolPatches = []
verbose = False
buildDebug = False
def parse_commandline():
global projDir, buildDir, moduleName, outFile, verbose, buildDebug
if len(sys.argv) < 3:
print("Please specify a project directory and a dol to link to!")
return False
# Set project directory
projDir = sys.argv[1]
if projDir.endswith("\\") or projDir.endswith("/"):
projDir = projDir[:-1]
buildDir = "%s\\build" % projDir
# Read DOL
dolFile.read(sys.argv[2])
if dolFile.buildVersion >= 3:
print("The specified dol file belongs to a Wii version of the game. The Wii versions are currently not supported.")
return False
if not dolFile.load_symbols(primeApiRoot + "\\symbols"):
return False
# Check other arguments
argIdx = 3
while argIdx < len(sys.argv):
arg = sys.argv[argIdx]
if arg == "-debug":
buildDebug = True
elif arg == "-m":
moduleName = sys.argv[argIdx + 1]
argIdx += 1
elif arg == "-o":
outFile = sys.argv[argIdx + 1]
argIdx += 1
elif arg == "-v":
verbose = True
argIdx += 1
# Set default values for some arguments
if not outFile:
outFile = "%s\\%s.rel" % (buildDir, moduleName)
return True
def get_extension(sourceFile):
return os.path.splitext(sourceFile)[1]
def get_object_path(sourceFile):
return "%s\\%s.o" % (buildDir, os.path.splitext( os.path.split(sourceFile)[1] )[0])
def generate_patch_code():
template = open("%s\\script\\ApplyCodePatches_Template.cpp" % primeApiRoot, "r")
code = template.read()
template.close()
# Find memory locations that need patches
sortedPatches = sorted(dolPatches, key=lambda patch: patch['address'])
classDecls = ""
funcDecls = ""
funcCode = ""
usedSymbols = []
usedSymbols = []
for patch in dolPatches:
address = patch['address']
symbol = patch['symbol']
type = patch['type']
# For function pointers, use static_cast to ensure the correct overload is used
argsStart = symbol.find('(')
if argsStart != -1:
argsEnd = symbol.rfind(')')
assert(argsEnd != -1)
funcName = symbol[0:argsStart]
params = symbol[argsStart+1:argsEnd]
symbolRef = "static_cast<void (*)(%s)>(&%s)" % (params, funcName)
else:
symbolRef = "&%s" % symbol
# Generate code for this patch
if type == R_PPC_REL24:
patchCode = '\n\tRelocate_Rel24((void*) 0x%08X, %s);' % (address, symbolRef)
elif type == R_PPC_ADDR32:
patchCode = '\n\tRelocate_Addr32((void*) 0x%08X, %s);' % (address, symbolRef)
else:
continue
if not symbol in usedSymbols:
paramsStart = symbol.find('(')
isFunction = paramsStart != -1
if isFunction:
# Add forward decl for classes referenced in the function parameters
paramsEnd = symbol.rfind(')')
params = split_params(symbol[paramsStart+1:paramsEnd])
for param in params:
paramStr = ''.join([chr for chr in param if not chr in "*&"])
paramWords = paramStr.split(' ')
for word in paramWords:
if word and word != 'const' and word != 'unsigned' and word != 'signed':
# this is probably our type name!
if not is_basic_type(word):
classDecls += "class %s;\n" % word
break
# Add forward decl for function name, accounting for scoping. This allows us to obtain the pointer to member functions
# of classes declared in other source files. It would not normally be allowed to declare a namespace with the same name as an
# existing class, but since none of our compiled modules know about each other, we can compile them and generate the same
# symbol as the member function, which will then be resolved by the linker!
funcDecl = ""
scopes = split_scopes(symbol)
funcName = scopes[-1]
del scopes[-1]
for scope in scopes:
funcDecl += "namespace %s { " % scope
funcDecl += "void %s;" % funcName
for scope in scopes:
funcDecl += " }"
funcDecl += "\n"
funcDecls += funcDecl
usedSymbols.append(symbol)
funcCode = funcCode + patchCode
cpptext = code % (classDecls + funcDecls, funcCode)
# Save file
cppname = "%s\\ApplyCodePatches.cpp" % buildDir
out = open(cppname, "w")
out.write(cpptext)
out.close()
return cppname
def parse_code_macros(sourcePath):
# Parse file, look for PATCH_SYMBOL macros
# This implementation leaves a bit to be desired - namely, it doesn't account for commented-out
# code or #ifdef'd out code, and won't correctly mangle templates that have default parameters
sourceFile = open(sourcePath, 'r')
regex = re.compile("^PATCH_SYMBOL\((.*)\((.*)\)(.*),(.*)\((.*)\)(.*)\)")
pos = 0
while True:
line = sourceFile.readline()
if not line: break
line = line.strip().replace(' ', '')
match = regex.search(line)
if match is not None:
origSymbol = "%s(%s)%s" % (match.group(1), match.group(2), match.group(3))
patchSymbol = "%s(%s)%s" % (match.group(4), match.group(5), match.group(6))
pos = match.endpos
dolPatches.extend( dolFile.generate_patches(mangle(origSymbol), patchSymbol) )
def compile_object(sourcePath, outPath):
# Create output directory
if not os.path.exists(outPath):
os.makedirs(outPath)
parse_code_macros(sourcePath)
# Check extension
print("Compiling %s" % sourcePath)
ext = get_extension(sourcePath)
if ext == ".cpp": lang = "c++"
elif ext == ".c": lang = "c"
else:
print("%s: unrecognized extension (%s)" % (sourcePath, ext))
return False
# Compile
includeDirs = ["-i-", # mark the include directories as system paths
"-I%s\\include" % primeApiRoot,
"-I%s\\include" % dolphinRoot,
"-I%s\\PowerPC_EABI_Support\\Runtime\\Inc" % cwRoot,
"-ir %s\\PowerPC_EABI_Support\\Msl\\MSL_C" % cwRoot]
if lang == "c++":
includeDirs.append("-ir %s\\PowerPC_EABI_Support\\Msl\\MSL_C++" % cwRoot)
args = [compilerPath,
"-align powerpc",
"-enum int",
"-proc gekko",
"-fp hardware",
"-lang %s" % lang,
"-Cpp_exceptions off",
"-sdata 0",
"-sdata2 0",
"-O",
" ".join(includeDirs),
"-r",
"-c %s" % sourcePath,
"-o %s" % get_object_path(sourcePath)]
# Run compiler
command = ' '.join(args)
if verbose:
print(">>> %s" % command)
ret = subprocess.call(command)
return ret == 0
def link_objects(objList):
print("Linking objects")
args = [linkerPath,
' '.join(objList),
"-unused",
"-r",
"-fp hardware",
"-map %s\\%s.map" % (buildDir, moduleName),
"-m _prolog",
"-lcf %s\\include\\dolphin\\eppc.lcf" % dolphinRoot,
"-o %s\\%s.preplf" % (buildDir, moduleName)]
# Run linker
command = ' '.join(args)
if verbose:
print(">>> %s" % command)
ret = subprocess.call(command)
return ret == 0
def convert_preplf_to_rel(preplfPath, outRelPath):
preplf = PreplfFile(preplfPath)
rel = OutputStream()
# Initial header info
rel.write_long(2) # Module ID. Hardcoding 2 here because 0 is the dol and the game already has a module using 1 (NESemu.rel). Should be more robust ideally
rel.write_long(0) # Next module link - always 0
rel.write_long(0) # Prev module link - always 0
rel.write_long(len(preplf.sections)) # Num sections
rel.write_long(0) # Section info offset filler
rel.write_long(0) # Module name offset (our rel won't include the module name so this is staying null)
rel.write_long(0) # Module name size
rel.write_long(2) # Module version
# Fetch data needed for the rest of the header
bssSec = preplf.section_by_name(".bss")
assert(bssSec != None)
prologSymbol = preplf.symbol_by_name("_prolog")
if prologSymbol is None:
prologSymbol = preplf.symbol_by_name("_prolog__Fv")
epilogSymbol = preplf.symbol_by_name("_epilog")
if epilogSymbol is None:
epilogSymbol = preplf.symbol_by_name("_epilog__Fv")
unresolvedSymbol = preplf.symbol_by_name("_unresolved")
if unresolvedSymbol is None:
unresolvedSymbol = preplf.symbol_by_name("_unresolved__Fv")
# Remaining header data
rel.write_long(bssSec.size)
rel.write_long(0) # Relocation table offset filler
rel.write_long(0) # Imports offset filler
rel.write_long(0) # Imports size filler
rel.write_byte(prologSymbol['sectionIndex'] if prologSymbol is not None else 0)
rel.write_byte(epilogSymbol['sectionIndex'] if epilogSymbol is not None else 0)
rel.write_byte(unresolvedSymbol['sectionIndex'] if unresolvedSymbol is not None else 0)
rel.write_byte(0) # Padding
rel.write_long(prologSymbol['value'] if prologSymbol is not None else 0)
rel.write_long(epilogSymbol['value'] if epilogSymbol is not None else 0)
rel.write_long(unresolvedSymbol['value'] if unresolvedSymbol is not None else 0)
rel.write_long(8) # Module alignment
rel.write_long(8) # BSS alignment
# Section info filler
sectionInfoOffset = rel.tell()
for section in preplf.sections:
rel.write_long(0)
rel.write_long(0)
# Write sections
sectionInfoList = []
wroteBss = False
for section in preplf.sections:
# Sections not in the to-keep list should be nulled out
info = {}
info['exec'] = (section.flags & 0x4) != 0
secStart = rel.tell()
name = section.name
isBss = name == ".bss"
shouldKeep = (name == ".text" or name == ".rodata" or name == ".ctors" or name == ".dtors" or name == ".data")
if shouldKeep is True:
info['offset'] = secStart
rel.write_bytes(section.data)
rel.write_to_boundary(4)
info['size'] = rel.tell() - secStart
elif isBss is True:
info['offset'] = 0
info['size'] = section.size
wroteBss = True
else:
assert(not name or wroteBss is True)
info['offset'] = 0
info['size'] = 0
sectionInfoList.append(info)
# Generate imports and write imports section filler
imports = []
moduleImports = { 'moduleID': 2, 'relocsOffset': 0 }
dolImports = { 'moduleID': 0, 'relocsOffset': 0 }
imports.append(moduleImports)
imports.append(dolImports)
importsOffset = rel.tell()
for importIdx in range(0, len(imports)):
rel.write_long(0)
rel.write_long(0)
importsSize = rel.tell() - importsOffset
# Write relocations
relocsOffset = rel.tell()
relocWriteSuccess = True
for importIdx in range(0, 2):
imports[importIdx]['relocsOffset'] = rel.tell()
isDol = imports[importIdx]['moduleID'] == 0
for section in preplf.sections:
if section.type != EST_RELA or len(section.relocs) == 0:
continue
symbolSection = section.link
targetSection = section.targetSecIdx
curOffset = 0
wroteSectionCommand = False
# Parse relocations
for reloc in section.relocs:
symbol = preplf.fetch_symbol(symbolSection, reloc['symbolIdx'])
assert(symbol != None)
# DOL relocations have a section index of 0; internal relocations have a valid section index
if (symbol['sectionIndex'] == 0) != isDol:
continue
# This is a valid relocation, so we have at least one - write the "change section" directive
if not wroteSectionCommand:
rel.write_short(0)
rel.write_byte(R_DOLPHIN_SECTION)
rel.write_byte(targetSection)
rel.write_long(0)
wroteSectionCommand = True
offset = reloc['offset'] - curOffset
# Add "nop" directives to make sure the offset will fit
# NOTE/TODO - not sure if this is actually supposed to be a signed offset - that needs to be verified
while offset > 0xFFFF:
rel.write_short(0xFFFF)
rel.write_byte(R_DOLPHIN_NOP)
rel.write_byte(0)
rel.write_long(0)
offset -= 0xFFFF
curOffset += 0xFFFF
# Write relocation
rel.write_short(offset)
rel.write_byte(reloc['relocType'])
# Internal relocs are easy - just copy data from the ELF reloc/symbol
if not isDol:
rel.write_byte(symbol['sectionIndex'])
rel.write_long(symbol['value']) # this is basically just the section-relative offset to the symbol
# DOL relocs will require looking up the address of the symbol in the DOL
else:
symbolName = symbol['name']
dolSymbolAddr = dolFile.get_symbol(symbolName)
if dolSymbolAddr is None:
print("Error: Failed to locate dol symbol: %s" % symbolName)
rel.write_byte(0)
rel.write_long(0)
relocWriteSuccess = False
continue
dolSymbolSection = dolFile.get_section_index(dolSymbolAddr)
rel.write_byte(dolSymbolSection)
rel.write_long(dolSymbolAddr)
curOffset += offset
# Write "end" directive
rel.write_short(0)
rel.write_byte(R_DOLPHIN_END)
rel.write_byte(0)
rel.write_long(0)
# Quit out?
if not relocWriteSuccess:
return False
# Write filler values from the header
rel.goto(0x10)
rel.write_long(sectionInfoOffset)
rel.goto(0x24)
rel.write_long(relocsOffset)
rel.write_long(importsOffset)
rel.write_long(importsSize)
rel.goto(sectionInfoOffset)
for section in sectionInfoList:
# Toggle 0x1 bit on the section offset for sections containing executable code
offset = section['offset']
if section['exec'] is True: offset |= 0x1
rel.write_long(offset)
rel.write_long(section['size'])
rel.goto(importsOffset)
for imp in imports:
rel.write_long(imp['moduleID'])
rel.write_long(imp['relocsOffset'])
# Done
rel.save_file(outRelPath)
print("Saved REL to %s" % outRelPath)
return True
def compile_rel():
sourceFiles = []
sourceFiles.extend( glob.glob("%s\\*.cpp" % projDir) )
sourceFiles.extend( glob.glob("%s\\*.c" % projDir) )
sourceFiles.append( "%s\\PowerPC_EABI_Support\\Runtime\\Src\\global_destructor_chain.c" % cwRoot )
sourceFiles = [file for file in sorted(sourceFiles) if file.find("build\\ApplyCodePatches.cpp") == -1]
# Compile/link source files
objectFiles = []
for sourceFile in sourceFiles:
if not compile_object(sourceFile, buildDir):
return False
else:
objectFiles.append(get_object_path(sourceFile))
if verbose: print('')
# Generate patching code
generatedFile = generate_patch_code()
genBuildSuccess = compile_object(generatedFile, buildDir)
if not genBuildSuccess:
return False
else:
objectFiles.append(get_object_path(generatedFile))
if verbose: print('')
# Link
if not link_objects(objectFiles):
return False
# We now have a .preplf file in the build folder... final step is to convert it to .rel
if not convert_preplf_to_rel("%s\\%s.preplf" % (buildDir, moduleName), outFile):
return False
return True
def main():
# Verify there is a valid installation of the Dolphin SDK and CodeWarrior
global dolphinRoot, cwRoot
dolphinRoot = os.getenv("DOLPHIN_ROOT")
cwRoot = os.getenv("CWFOLDER")
if dolphinRoot is None:
print("Error: The DOLPHIN_ROOT environment variable is undefined. BuildModule.py requires an installation of the Dolphin SDK.")
if cwRoot is None:
print("Error: The CWFOLDER environment variable is undefined. BuildModule.py requires an installation of CodeWarrior version 2.3.3.")
if dolphinRoot is None or cwRoot is None:
return
# Set globals
global cwToolsDir, compilerPath, linkerPath
cwToolsDir = cwRoot + "\\PowerPC_EABI_Tools\\Command_Line_Tools"
compilerPath = cwToolsDir + "\\mwcceppc.exe"
linkerPath = cwToolsDir + "\\mwldeppc.exe"
# Parse commandline argments
if not parse_commandline():
return
# Apply DOL patches
if not dolFile.is_patched():
print("Detected DOL file is unpatched. Applying patch...")
dolFilename = dolFile.filename
nameEnd = dolFilename.rfind('.')
nameBase = dolFilename[0:nameEnd]
patchedName = "%s_mod.dol" % nameBase
patchFile = "%s\\script\\DolPatch.bin" % primeApiRoot
dolFile.apply_patch(patchFile, patchedName)
print("Saved patched DOL to %s" % patchedName)
# Compile
compileSuccess = compile_rel()
print("***** COMPILE %s! *****" % ("SUCCEEDED" if compileSuccess else "FAILED"))
if __name__ == "__main__":
main()