mirror of https://github.com/n64decomp/007.git
734 lines
30 KiB
Python
734 lines
30 KiB
Python
import datetime
|
|
import errno
|
|
import functools
|
|
import getopt
|
|
import os
|
|
import re
|
|
import shlex
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
"""
|
|
re-writing the original ge-stats py. - Ben Burns, Dec 18, 2021
|
|
|
|
The general approach is to scan for source files in the specified directories (*.s, *.c).
|
|
Each file is evaluated, and a list of assembly function definitions is generated.
|
|
The map file is then parsed, giving a list of all known functions.
|
|
Stats are then computed for asm functions out of all known functions.
|
|
|
|
The logic to determine whether a function has an assembly definition is rather simple.
|
|
It doesn't handle any c preprocessor macros, so the results are not guaranteed to
|
|
be accurate. If a line begins with the text "glabel" and is only followed by one
|
|
word, that is assumed to be an assembly function definition. For the purposes
|
|
of this script, any assembly definition for any ROM version will count against
|
|
the total.
|
|
"""
|
|
|
|
# lower case
|
|
__supported_versions = ['us', 'jp', 'eu']
|
|
|
|
# any function read from .map file exceeding this size will throw an exception
|
|
__invalid_func_length_size = 1000000
|
|
|
|
# attempt to filter out extra glabels that are not function definitions. This is for jump table entries
|
|
__re_jpt_label = re.compile(r"\.L[0-9a-fA-F]{8}")
|
|
|
|
# report output paths
|
|
__report_dir = "tools" + os.path.sep + "report"
|
|
__report_bin = __report_dir + os.path.sep + "report"
|
|
__report_template = __report_dir + os.path.sep + "template.html"
|
|
__report_out_us = __report_dir + os.path.sep + "index.html"
|
|
__report_out_jp = __report_dir + os.path.sep + "JPN.htm"
|
|
__report_out_eu = __report_dir + os.path.sep + "EU.htm"
|
|
|
|
# default fallback if recently modified timestamps don't work
|
|
__mtime_fallback_filename = 'ge.u.z64'
|
|
|
|
class FunctionInfo:
|
|
"""
|
|
Info about a function
|
|
"""
|
|
def __init__(self, name: str):
|
|
# function name
|
|
self.name = name
|
|
# flag to indicate if an asm function definition exists
|
|
self.nonmatching = False
|
|
# length of function as computed from map file
|
|
self.length = 0
|
|
# parent SourceFileContent
|
|
self.parent = None
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def __repr__(self):
|
|
return self.name
|
|
|
|
|
|
class SourceFileContent:
|
|
"""
|
|
Information about a source file
|
|
"""
|
|
def __init__(self, path: str):
|
|
# path to file
|
|
self.path = path
|
|
# list of all asm function definitions found in the file
|
|
self.asm_functions = set()
|
|
# list of FunctionInfo as parsed from map file
|
|
self.all_functions = []
|
|
|
|
if path.endswith(".c") or path.endswith(".s") or path.endswith(".o"):
|
|
self.path_without_ext = path[:-2]
|
|
else:
|
|
self.path_without_ext = path
|
|
|
|
# reference to SearchDir
|
|
self.parent = None
|
|
# last modified time, in seconds since epoch
|
|
self.mtime = 0
|
|
|
|
def __str__(self):
|
|
return self.path
|
|
|
|
def __repr__(self):
|
|
return self.path
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, SourceFileContent):
|
|
# don't attempt to compare against unrelated types
|
|
return NotImplemented
|
|
|
|
return self.path == other.path
|
|
|
|
class SearchDir:
|
|
"""
|
|
container to define search paramaters.
|
|
"""
|
|
def __init__(self, path: str, recurse: bool, ignore = None, completed = None):
|
|
"""
|
|
default constructor
|
|
|
|
:param str path: folder path, relative to root where script is run. Should not
|
|
contain leading or trailing slash.
|
|
:param bool recurse: whether or not to recursively search contents of path.
|
|
:param ignore: optional files to ignore. List of strings. This is an explicit list of
|
|
files with any necessary folder prefix. Do not include leading slash. Wildcards
|
|
are not supported.
|
|
:param completed: optional files to mark as completed. List of strings. This is an
|
|
explicit list of files with any necessary folder prefix. Do not include leading
|
|
slash. Wildcards are not supported.
|
|
"""
|
|
self.path = path
|
|
self.recurse = not not recurse
|
|
# (config option) ignored files won't be added to source files
|
|
if ignore is None:
|
|
ignore = []
|
|
self.ignore = ignore
|
|
# (config option) completed files won't have asm definitions added
|
|
if completed is None:
|
|
completed = []
|
|
self.completed = completed
|
|
|
|
# (runtime stat) count of functions found in/under this directory
|
|
self.function_count = 0
|
|
# (runtime stat) length in bytes for all functions found
|
|
self.function_byte_count = 0
|
|
# (runtime stat) count of non matching functions found in/under this directory
|
|
self.nonmatching_count = 0
|
|
# (runtime stat) length in bytes for all non matching functions found
|
|
self.nonmatching_byte_count = 0
|
|
# (runtime stat) count of found files
|
|
self.file_count = 0
|
|
# (runtime stat) count of found files with no asm definitions
|
|
self.completed_file_count = 0
|
|
|
|
# fix path seperator if this is run on windows os (visual only, it will still run...)
|
|
self.path = self.path.replace('/', os.path.sep)
|
|
|
|
def __str__(self):
|
|
return self.path
|
|
|
|
def __repr__(self):
|
|
return self.path
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, SearchDir):
|
|
# don't attempt to compare against unrelated types
|
|
return NotImplemented
|
|
|
|
return self.path == other.path
|
|
|
|
|
|
class StatResults:
|
|
def __init__(self):
|
|
# "now" starting point, modification times must be before this datetime.
|
|
# Only applies to git log, OS modified time is unaffected.
|
|
self.now = datetime.datetime.now()
|
|
self.search_dirs = []
|
|
# reference to SourceFileContent with highest mtime
|
|
self.last_modified_file = None
|
|
# status flag. Used to determine if last modified timestamps are valid.
|
|
self.last_mtime_valid = False
|
|
# count of all functions
|
|
self.total_function_count = 0
|
|
# length in bytes for all functions
|
|
self.total_function_byte_count = 0
|
|
# count of non matching functions
|
|
self.total_nonmatching_count = 0
|
|
# length in bytes for all non matching functions
|
|
self.total_nonmatching_byte_count = 0
|
|
# count of found files with no asm definitions
|
|
self.total_completed_file_count = 0
|
|
# list of SourceFileContent
|
|
self.source_files = []
|
|
|
|
|
|
|
|
def mtime_os(file, now, qlocal = False):
|
|
if not qlocal:
|
|
print('using os')
|
|
return int(os.path.getmtime(file))
|
|
|
|
|
|
|
|
def mtime_git(file, now, qlocal = False):
|
|
if not qlocal:
|
|
print('using git')
|
|
try:
|
|
date_str = now.strftime('%Y-%m-%dT%H:%M:%S%z')
|
|
result = subprocess.run(['git', 'log', '-1', '--format=\"%ct\"', '--date=local', '--before=\"' + date_str + '\"', '--', file], stdout=subprocess.PIPE, universal_newlines=True)
|
|
except:
|
|
print ('fatal error reading git log history, maybe use OS modified time resolver, --mtime_os option. File: "' + file + '", date_str: "' + date_str + '"')
|
|
sys.exit(7)
|
|
|
|
log_out = result.stdout.strip().replace('"', '')
|
|
if not log_out:
|
|
return 0
|
|
return int(log_out)
|
|
|
|
|
|
def process_source_files(search, stats: StatResults, mtime_resolver, qlocal = False):
|
|
|
|
stats.search_dirs = search
|
|
|
|
# regular expression: a line that starts with text "glabel", followed by
|
|
# a single space, followed by (captured) anything that is not whitespace
|
|
# one or more times, followed by end of line.
|
|
p1 = re.compile(r"^glabel (\S+)$")
|
|
|
|
# iterate each search location, looking inside source files for asm function definitions
|
|
for s in search:
|
|
if not os.path.isdir(s.path):
|
|
print('fatal: directory not found:', s.path)
|
|
sys.exit(3)
|
|
|
|
for root, dir_names, filenames in os.walk(s.path):
|
|
for filename in filenames:
|
|
file = os.path.join(root, filename)
|
|
|
|
if not (filename.lower().endswith(".c") or filename.lower().endswith(".s")):
|
|
continue
|
|
|
|
if filename in s.ignore:
|
|
continue
|
|
|
|
#print(file)
|
|
|
|
sfc = SourceFileContent(file)
|
|
|
|
sfc.asm_functions = set()
|
|
sfc.parent = s
|
|
sfc.mtime = mtime_resolver(file, stats.now, qlocal)
|
|
import datetime
|
|
|
|
x = datetime.datetime.fromtimestamp(sfc.mtime)
|
|
if not qlocal:
|
|
print(sfc.path, ' = ', x.strftime('%c'))
|
|
|
|
# The `completed` list is manually configured to specify which files should not be
|
|
# counted against the total. This is done by simply not adding asm function
|
|
# definitions into the asm_functions property.
|
|
if filename not in s.completed:
|
|
with open(file) as fp:
|
|
# The "in" operator will be faster than regex here, but will include stuff
|
|
# we don't want. Do an initial pass with "in", then refine results with
|
|
# regex.
|
|
# The container is a hashset for cases where the function is
|
|
# declared multiple times (e.g., different versions)
|
|
file_glabel_lines = [line for line in fp.readlines() if "glabel" in line]
|
|
for line in file_glabel_lines:
|
|
r1 = p1.findall(line)
|
|
if r1 and len(r1) == 1 and not re.match(__re_jpt_label, r1[0]):
|
|
sfc.asm_functions.add(r1[0])
|
|
|
|
stats.source_files.append(sfc)
|
|
|
|
if not s.recurse:
|
|
break
|
|
|
|
return stats
|
|
|
|
|
|
def apply_build_map(stats: StatResults, version: str):
|
|
"""
|
|
This iterates over the build map file.
|
|
It scans for the start of a `.text` definition indicating a new file.
|
|
Then it parses each following line to get function names within the file
|
|
until a new section is encountered.
|
|
"""
|
|
version_code = version[0]
|
|
map_file_path = 'build' + os.path.sep + version_code + os.path.sep + 'ge007.' + version_code + '.map'
|
|
|
|
infile = False
|
|
prevromaddr = 0
|
|
|
|
# regex:
|
|
# a line that starts with text " .text", followed by any whitespace (at least one), then a 64 bit hex address.
|
|
# the lower 32 bits of the address are captured in a group. Followed by any whitespace (at least one),
|
|
# followed by a hex address (capturing everything after "0x"), followed by any whitespace (at least one).
|
|
# It then matches a build path, based on version, capturing everything after the version and slash
|
|
# up to the file extension, followed by end of line.
|
|
p1 = re.compile(r"^ \.text\s+0x00000000([0-9a-f]{8})\s+0x([0-9a-f]+)\s+build\/" + re.escape(version_code) + r"\/([^\s]+)$")
|
|
|
|
# regex to match subsequent lines after the above regex matches. This will
|
|
# match any whitespace, then 64 bit address (capturing the lower 32 bits), then
|
|
# any whitespace (at least one), then capture the remaining non-whitespace-text until end of line (in capture group).
|
|
# That last capture group is the function name.
|
|
p2 = re.compile(r"^\s+0x00000000([0-9a-f]{8})\s+(\S+)$")
|
|
|
|
current_sfc = None
|
|
current_fi = None
|
|
|
|
with open(map_file_path, 'r') as map_file:
|
|
lines = map_file.readlines()
|
|
|
|
for line in lines:
|
|
|
|
m1 = p1.findall(line)
|
|
m2 = p2.findall(line)
|
|
|
|
# m1 match means this is a new .text section (new file)
|
|
if m1:
|
|
filestart = int(m1[0][0], 16)
|
|
filelen = int(m1[0][1], 16)
|
|
# normalize paths
|
|
segment = (m1[0][2]).replace('/', os.path.sep)
|
|
infile = True
|
|
prevromaddr = 0
|
|
prevfuncname = None
|
|
|
|
current_fi = None
|
|
|
|
if segment.endswith(".c") or segment.endswith(".s") or segment.endswith(".o"):
|
|
segment_without_ext = segment[:-2]
|
|
else:
|
|
segment_without_ext = segment
|
|
|
|
# get the SourceFileContent container for this file listing
|
|
current_sfc = next((x for x in stats.source_files if x.path_without_ext == segment_without_ext), None)
|
|
# if there's no existing reference, that means this function is listed
|
|
# in the map file but not found when searching directories previously.
|
|
# In that case, this file/function should be ignored.
|
|
if current_sfc is None:
|
|
infile = False
|
|
|
|
elif infile and m2:
|
|
romaddr = int(m2[0][0], 16)
|
|
funcname = m2[0][1]
|
|
|
|
if re.match(__re_jpt_label, funcname):
|
|
continue
|
|
|
|
if current_sfc is None:
|
|
print('invalid state: current_sfc not set')
|
|
sys.exit(4)
|
|
|
|
if current_fi is not None:
|
|
current_fi.length = romaddr - prevromaddr
|
|
if current_fi.length > __invalid_func_length_size:
|
|
raise ValueError("function length too large: " + str(funcname) + " len = " + str(current_fi.length))
|
|
|
|
# get the function info for this function, or create a new one
|
|
current_fi = next((x for x in current_sfc.all_functions if x.name == funcname), None)
|
|
if current_fi is None:
|
|
current_fi = FunctionInfo(funcname)
|
|
current_fi.parent = current_sfc
|
|
# check to see if there was an asm definition for this function
|
|
if funcname in current_sfc.asm_functions:
|
|
current_fi.nonmatching = True
|
|
current_sfc.all_functions.append(current_fi)
|
|
|
|
prevromaddr = romaddr
|
|
prevfuncname = funcname
|
|
|
|
elif infile:
|
|
infile = False
|
|
|
|
# this is a one line definition in the map file
|
|
if current_fi is None:
|
|
current_fi = FunctionInfo(funcname)
|
|
current_fi.parent = current_sfc
|
|
# check to see if there was an asm definition for this function
|
|
if funcname in current_sfc.asm_functions:
|
|
current_fi.nonmatching = True
|
|
# since this is only one entry, use the entire size
|
|
current_fi.length = filelen
|
|
current_sfc.all_functions.append(current_fi)
|
|
else:
|
|
current_fi.length = (filestart + filelen) - prevromaddr
|
|
|
|
if current_fi.length > __invalid_func_length_size:
|
|
raise ValueError("function length too large: " + str(funcname) + " len = " + str(current_fi.length))
|
|
|
|
|
|
|
|
def percent_complete(num, den):
|
|
if not (den > 0):
|
|
return 1
|
|
elif num == den:
|
|
return 1
|
|
|
|
frac = num / den
|
|
|
|
if frac <= 0:
|
|
return 0
|
|
elif frac > 0 and frac < 0.01:
|
|
return 0.01
|
|
elif frac >= 0.01 and frac <= 0.99:
|
|
return frac
|
|
elif frac >= 0.99 and frac < 1:
|
|
return 0.99
|
|
|
|
raise ValueError("Can't compute percent complete: " + str(num) + " / " + str(den))
|
|
|
|
|
|
def generate_default_stats(stats: StatResults):
|
|
"""
|
|
Iterate each file, and each function in each file, to compute
|
|
the total bytes/functions/files in each searchdir, and sum
|
|
the totals for the entire search.
|
|
"""
|
|
|
|
# track unique modification times
|
|
seen_mtime = set()
|
|
|
|
for file in stats.source_files:
|
|
# if this file comes from the .map file and does not have the
|
|
# parent association setup then it should be ignored.
|
|
if file.parent is None:
|
|
continue
|
|
|
|
if stats.last_modified_file is None or stats.last_modified_file.mtime < file.mtime:
|
|
stats.last_modified_file = file
|
|
|
|
seen_mtime.add(file.mtime)
|
|
|
|
for func in file.all_functions:
|
|
file.parent.function_count += 1
|
|
file.parent.function_byte_count += func.length
|
|
stats.total_function_count += 1
|
|
stats.total_function_byte_count += func.length
|
|
|
|
if func.nonmatching:
|
|
file.parent.nonmatching_count += 1
|
|
file.parent.nonmatching_byte_count += func.length
|
|
stats.total_nonmatching_count += 1
|
|
stats.total_nonmatching_byte_count += func.length
|
|
|
|
file.parent.file_count += 1
|
|
|
|
if len(file.asm_functions) == 0:
|
|
file.parent.completed_file_count += 1
|
|
stats.total_completed_file_count += 1
|
|
|
|
# More hacks to check for valid modification times.
|
|
# 10 is an arbitrary number, but this says that if there are
|
|
# at least 10 unique modification times, assume the timestamps
|
|
# are valid. Otherwise this will fallbac kto show the default file.
|
|
if len(seen_mtime) > 10:
|
|
stats.last_mtime_valid = True
|
|
|
|
# If it wasn't possible to determine recently modified file (i.e., github actions)
|
|
# then fallback to "ge.u.z64"
|
|
if not stats.last_mtime_valid:
|
|
stats.last_modified_file = SourceFileContent(__mtime_fallback_filename)
|
|
|
|
|
|
def print_default_stats(stats: StatResults, version):
|
|
|
|
print()
|
|
print('~> Decomp Statistics for ' + version.upper() +' version')
|
|
print('__________________________________________________________________')
|
|
print()
|
|
|
|
print('FILES')
|
|
print('\trecently modified:')
|
|
if stats.last_mtime_valid:
|
|
recent_files = sorted(stats.source_files, key=lambda x: x.mtime, reverse=True)[:5]
|
|
for rf in recent_files:
|
|
print('\t\t' + rf.path)
|
|
else:
|
|
print('\t\t' + __mtime_fallback_filename)
|
|
print()
|
|
for s in stats.search_dirs:
|
|
print('\t{:12}\t{:5,} / {:5,} \t{:6.2f}%'.format(s.path, int(s.completed_file_count), int(s.file_count), (percent_complete(s.completed_file_count, s.file_count) * 100)))
|
|
print()
|
|
print('\ttotal \t{:5,} / {:5,} \t{:6.2f}%'.format(int(stats.total_completed_file_count), len(stats.source_files), (percent_complete(stats.total_completed_file_count, len(stats.source_files)) * 100)))
|
|
print('__________________________________________________________________')
|
|
print()
|
|
|
|
print('FUNCTIONS')
|
|
for s in stats.search_dirs:
|
|
d_completed_func_count = s.function_count - s.nonmatching_count
|
|
print('\t{:12}\t{:7,} / {:7,} \t{:6.2f}%'.format(s.path, int(d_completed_func_count), int(s.function_count), (percent_complete(d_completed_func_count, s.function_count) * 100)))
|
|
print()
|
|
print('\ttotal \t{:7,} / {:7,} \t{:6.2f}%'.format(int(stats.total_nonmatching_count), int(stats.total_function_count), (percent_complete(stats.total_nonmatching_count, stats.total_function_count) * 100)))
|
|
print('__________________________________________________________________')
|
|
print()
|
|
|
|
print('BYTES')
|
|
for s in stats.search_dirs:
|
|
d_completed_byte_count = s.function_byte_count - s.nonmatching_byte_count
|
|
print('\t{:12}\t{:11,} / {:11,} \t{:6.2f}%'.format(s.path, int(d_completed_byte_count), int(s.function_byte_count), (percent_complete(d_completed_byte_count, s.function_byte_count) * 100)))
|
|
print()
|
|
completed_byte_count = stats.total_function_byte_count - stats.total_nonmatching_byte_count
|
|
print('\ttotal \t{:11,} / {:11,} \t{:6.2f}%'.format(int(completed_byte_count), int(stats.total_function_byte_count), (percent_complete(completed_byte_count, stats.total_function_byte_count) * 100)))
|
|
print('__________________________________________________________________')
|
|
print()
|
|
|
|
|
|
|
|
def print_non_matching_stats(stats: StatResults, version):
|
|
"""
|
|
List all non matching functions. Sort by byte length.
|
|
Print as csv of filename, function name, function length.
|
|
"""
|
|
functions = []
|
|
for file in stats.source_files:
|
|
for func in file.all_functions:
|
|
if func.nonmatching:
|
|
functions.append(func)
|
|
functions.sort(key=lambda x: x.length, reverse=True)
|
|
try:
|
|
print("filename, function, length")
|
|
for f in functions:
|
|
print(f.parent.path + ", " + f.name + ", " + str(f.length))
|
|
except socket.error as e:
|
|
if e.errno == errno.EPIPE:
|
|
sys.exit(0)
|
|
raise
|
|
|
|
|
|
def bytes_to_words(b):
|
|
return (int(b) >> 2) << 2
|
|
|
|
|
|
def generate_report(stats: StatResults, version):
|
|
"""
|
|
Send values over to the c report app.
|
|
Arguments must be added in the following order:
|
|
|
|
note: changing from "libultra" to "libultrare"
|
|
|
|
SRC_DIR: int, size in 32-bit words of completed functions, in src/ directory
|
|
SRC_DIR_MAX: int, size in 32-bit words of all tracked functions, in src/ directory
|
|
GAME_DIR: int, size in 32-bit words of completed functions, in src/game/ directory
|
|
GAME_DIR_MAX: int, size in 32-bit words of all tracked functions, in src/game/ directory
|
|
INFLATE_DIR: int, size in 32-bit words of completed functions, in src/inflate/ directory
|
|
INFLATE_DIR_MAX: int, size in 32-bit words of all tracked functions, in src/inflate/ directory
|
|
LIBULTRA_DIR: int, size in 32-bit words of completed functions, in src/libultrare/ directory
|
|
LIBULTRA_DIR_MAX: int, size in 32-bit words of all tracked functions, in src/libultrare/ directory
|
|
DECOMPILED_WORDS: int, size in 32-bit words of all completed functions
|
|
DECOMPILED_WORDS_MAX: int, size in 32-bit words of all tracked functions
|
|
DECOMPILED_FILES: int, count of all fully completed files
|
|
DECOMPILED_FILES_MAX: int, count of all tracked files
|
|
HTML_TEMPLATE: string: path to html template input file
|
|
HTML_OUTPUT: string: path to generated html file
|
|
LAST_MODIFIED_FILE: string: name of last modified file
|
|
LOG_LEVEL: int (enum): LOG_MIN = 0, LOG_DEF, LOG_MAX
|
|
|
|
"""
|
|
|
|
sd = next((x for x in stats.search_dirs if x.path == 'src'), None)
|
|
if sd is None:
|
|
raise ValueError('Could not find "src" SearchDir')
|
|
src_dir = bytes_to_words(sd.function_byte_count - sd.nonmatching_byte_count)
|
|
src_dir_max = bytes_to_words(sd.function_byte_count)
|
|
|
|
sd = next((x for x in stats.search_dirs if x.path == 'src/game'), None)
|
|
if sd is None:
|
|
raise ValueError('Could not find "src/game" SearchDir')
|
|
game_dir = bytes_to_words(sd.function_byte_count - sd.nonmatching_byte_count)
|
|
game_dir_max = bytes_to_words(sd.function_byte_count)
|
|
|
|
sd = next((x for x in stats.search_dirs if x.path == 'src/inflate'), None)
|
|
if sd is None:
|
|
raise ValueError('Could not find "src/inflate" SearchDir')
|
|
inflate_dir = bytes_to_words(sd.function_byte_count - sd.nonmatching_byte_count)
|
|
inflate_dir_max = bytes_to_words(sd.function_byte_count)
|
|
|
|
# note: libultrare
|
|
sd = next((x for x in stats.search_dirs if x.path == 'src/libultrare'), None)
|
|
if sd is None:
|
|
raise ValueError('Could not find "src/libultrare" SearchDir')
|
|
libultrare_dir = bytes_to_words(sd.function_byte_count - sd.nonmatching_byte_count)
|
|
libultrare_dir_max = bytes_to_words(sd.function_byte_count)
|
|
|
|
decompiled_words = bytes_to_words(stats.total_function_byte_count - stats.total_nonmatching_byte_count)
|
|
decompiled_words_max = bytes_to_words(stats.total_function_byte_count)
|
|
|
|
decompiled_files = stats.total_completed_file_count
|
|
decompiled_files_max = len(stats.source_files)
|
|
|
|
if version == 'us':
|
|
html_output = __report_out_us
|
|
elif version == 'jp':
|
|
html_output = __report_out_jp
|
|
elif version == 'eu':
|
|
html_output = __report_out_eu
|
|
else:
|
|
print('fatal: version', version, 'not supported! Supported versions are: ', ', '.join(__supported_versions))
|
|
sys.exit(2)
|
|
|
|
printstring = __report_bin + ' ' + \
|
|
str(src_dir) + ' ' + \
|
|
str(src_dir_max) + ' ' + \
|
|
str(game_dir) + ' ' + \
|
|
str(game_dir_max) + ' ' + \
|
|
str(inflate_dir) + ' ' + \
|
|
str(inflate_dir_max) + ' ' + \
|
|
str(libultrare_dir) + ' ' + \
|
|
str(libultrare_dir_max) + ' ' + \
|
|
str(decompiled_words) + ' ' + \
|
|
str(decompiled_words_max) + ' ' + \
|
|
str(decompiled_files) + ' ' + \
|
|
str(decompiled_files_max) + ' ' + \
|
|
__report_template + ' ' + \
|
|
html_output + ' ' + \
|
|
stats.last_modified_file.path + ' ' + \
|
|
'0'
|
|
|
|
subprocess.Popen(printstring.split())
|
|
|
|
|
|
def print_help():
|
|
print('Usage: python3 ge-stats2.py [OPTION]')
|
|
print('Generate Decompilation Stats for Goldeneye 007')
|
|
print('Example: python3 ge-stats2.py --version us')
|
|
print(' ')
|
|
print(' This script expects to be run from the root directory. There must be')
|
|
print(' a map file in ./build/u/ge007.u.map (or equivalent version). Source files')
|
|
print(' must be located in a folder in the root directory.')
|
|
print(' ')
|
|
print(' This script does not attempt to parse preprocessor definitions and')
|
|
print(' ignores version differences in source files. For the purposes of these')
|
|
print(' stats, any version with an asm definition of a function will count')
|
|
print(' against completion. However, a version is still required to specify')
|
|
print(' which build map to reference against.')
|
|
print(' ')
|
|
print('Options:')
|
|
print(' -v, --version=CODE generate decomp stats for version CODE (us, jp, or eu). Default is us')
|
|
print(' -r, --report generate html report using report tool')
|
|
print(' -n, --non_matching print csv of all non matching function definitions')
|
|
print(' --mtime_os use OS last modified time instead of git log')
|
|
print(' -h, --help display this help text and exit')
|
|
print(' --qlocal quiet "local" version. Disable all the extra print statements included for github actions.')
|
|
|
|
def main():
|
|
# default to US version
|
|
version = 'us'
|
|
run_report = False
|
|
print_method = 'default'
|
|
mtime_use_os = False
|
|
qlocal = False
|
|
|
|
if (len(sys.argv) > 1):
|
|
arguments, values = getopt.getopt(sys.argv[1:], "hv:rn", ["help", "version=", "report", "non_matching", "mtime_os", "qlocal"])
|
|
|
|
for current_argument, current_value in arguments:
|
|
if current_argument in ("-h", "--help"):
|
|
print_help()
|
|
sys.exit()
|
|
elif current_argument in ("-v", "--version"):
|
|
version = current_value.lower()
|
|
elif current_argument in ("-r", "--report"):
|
|
run_report = True
|
|
elif current_argument in ("-n", "--non_matching"):
|
|
print_method = 'non_matching'
|
|
elif current_argument in ("--mtime_os"):
|
|
mtime_use_os = True
|
|
elif current_argument in ("--qlocal"):
|
|
qlocal = True
|
|
|
|
if version not in __supported_versions:
|
|
print('fatal: version', version, 'not supported! Supported versions are: ', ', '.join(__supported_versions))
|
|
sys.exit(2)
|
|
|
|
if run_report:
|
|
if not os.path.isfile(__report_bin):
|
|
print('fatal: file not found: ' + __report_bin)
|
|
sys.exit(6)
|
|
|
|
# if this is just getting status on nonmatching files, ignore modified time lookup.
|
|
if print_method == 'non_matching' and not run_report:
|
|
mtime_use_os = True
|
|
|
|
if not qlocal:
|
|
print(subprocess.run(['git', 'diff', 'origin/master', '--name-only', '"@{10 minutes ago}"']).stdout)
|
|
# Default to using git log to get the file's modified date.
|
|
# Git log will be much slower, but cloning a new repo (i.e., github actions online)
|
|
# will reset all the modified timestamps to the same value, so will need to
|
|
# resolve the last modified time from git history.
|
|
mtime_resolver = mtime_os
|
|
if not mtime_use_os:
|
|
if os.path.isdir('.git'):
|
|
mtime_resolver = mtime_git
|
|
else:
|
|
# ok, do one last check (for github actions)
|
|
try:
|
|
result = subprocess.run(['git', 'log', '-1', '--format=\"%ct\"', '--', 'readme.md'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
|
|
if result.returncode == 0:
|
|
mtime_resolver = mtime_git
|
|
except:
|
|
pass
|
|
|
|
# files to count as complete, in src/ directory
|
|
src_completed_list = [
|
|
'_start.s',
|
|
'aspboot.s',
|
|
'bootcode.s',
|
|
'getra.s',
|
|
'gspboot.s',
|
|
'osMapTLB.s',
|
|
'rom_header.s',
|
|
'rspboot.s',
|
|
'tlb_hardwire.s',
|
|
'tlb_resolve.s']
|
|
|
|
search = []
|
|
search.append(SearchDir('src', False, completed=src_completed_list))
|
|
search.append(SearchDir('src/game', False))
|
|
search.append(SearchDir('src/inflate', False))
|
|
search.append(SearchDir('src/libultrare', True))
|
|
|
|
stats = StatResults()
|
|
|
|
process_source_files(search, stats, mtime_resolver, qlocal)
|
|
apply_build_map(stats, version)
|
|
generate_default_stats(stats)
|
|
|
|
if print_method == 'default':
|
|
print_default_stats(stats, version)
|
|
elif print_method == 'non_matching':
|
|
print_non_matching_stats(stats, version)
|
|
|
|
if run_report:
|
|
generate_report(stats, version)
|
|
|
|
if __name__ == '__main__':
|
|
#start_time = time.time()
|
|
main()
|
|
#print("--- %s seconds ---" % (time.time() - start_time))
|