* CMake build infrastructure The squashed commit that builds and packages releases for the SIMH simulator suite with CMake, version 3.14 or newer. See README-CMake.md for documentation.
411 lines
16 KiB
Python
411 lines
16 KiB
Python
## cmake_container.py
|
|
##
|
|
## A container for a collection of SIMH simulators
|
|
##
|
|
##
|
|
|
|
import sys
|
|
import os
|
|
import re
|
|
import pprint
|
|
import functools
|
|
|
|
import simgen.parse_makefile as SPM
|
|
import simgen.sim_collection as SC
|
|
import simgen.utils as SU
|
|
|
|
## Corresponding special variable uses in the makefile:
|
|
_special_sources = frozenset(['${DISPLAYL}', '$(DISPLAYL)'])
|
|
|
|
## Banner header for individual CMakeLists.txt files:
|
|
_individual_header = [
|
|
'##',
|
|
'## This is an automagically generated file. Do NOT EDIT.',
|
|
'## Any changes you make will be overwritten!!',
|
|
'##',
|
|
'## Make changes to the SIMH top-level makefile and then run the',
|
|
'## "cmake/generate.py" script to regenerate these files.',
|
|
'##',
|
|
'## cd cmake; python -m generate --help',
|
|
'##',
|
|
'## ' + '-' * 60 + '\n'
|
|
]
|
|
|
|
## Banner header for individual unit test CMakeLists.txt files:
|
|
_unit_test_header = [
|
|
'##',
|
|
'## This is an automagically generated file. Do NOT EDIT.',
|
|
'## Any changes you make will be overwritten!!',
|
|
'##',
|
|
'## If you need to make changes, modify write_unit_test()',
|
|
'## method in the the underlying Python class in ',
|
|
'## cmake/simgen/basic_simulator.py, then execute the',
|
|
'## "cmake/generate.py" script to regenerate these files.',
|
|
'##',
|
|
'## cd cmake; python -m generate --help',
|
|
'##',
|
|
'## ' + '-' * 60 + '\n'
|
|
]
|
|
## Unset the display variables when not building with video. A +10
|
|
## kludge.
|
|
_unset_display_vars = '''
|
|
if (NOT WITH_VIDEO)
|
|
### Hack: Unset these variables so that they don't expand if
|
|
### not building with video:
|
|
set(DISPLAY340 "")
|
|
set(DISPLAYIII "")
|
|
set(DISPLAYNG "")
|
|
set(DISPLAYVT "")
|
|
endif ()
|
|
'''
|
|
|
|
class CMakeBuildSystem:
|
|
"""A container for collections of SIMH simulators and automagic
|
|
CMakeLists.txt driver.
|
|
|
|
This is the top-level container that stores collections of SIMH simulators
|
|
in the 'SIM'
|
|
"""
|
|
|
|
## Compile command line elements that are ignored.
|
|
_ignore_compile_elems = frozenset(['${LDFLAGS}', '$(LDFLAGS)',
|
|
'${CC_OUTSPEC}', '$(CC_OUTSPEC)',
|
|
'${SIM}', '$(SIM)',
|
|
'-o', '$@',
|
|
'${NETWORK_OPT}', '$(NETWORK_OPT)',
|
|
'${SCSI}', '$(SCSI)',
|
|
'${DISPLAY_OPT}', '$(DISPLAY_OPT)',
|
|
'${VIDEO_CCDEFS}', '$(VIDEO_CCDEFS)',
|
|
'${VIDEO_LDFLAGS}', '$(VIDEO_LDFLAGS)',
|
|
'${BESM6_PANEL_OPT}', '$(BESM6_PANEL_OPT)'])
|
|
|
|
def __init__(self):
|
|
# "Special" variables that we look for in source code lists and which we'll
|
|
# emit into the CMakeLists.txt files.
|
|
self.vars = SC.ignored_display_macros.copy()
|
|
# Subdirectory -> SimCollection mapping
|
|
self.dirs = {}
|
|
|
|
|
|
def extract(self, compile_action, test_name, sim_dir, sim_name, defs, buildrom, debug=0, depth=''):
|
|
"""Extract sources, defines, includes and flags from the simulator's
|
|
compile action in the makefile.
|
|
"""
|
|
sim_dir_path = SPM.expand_vars(sim_dir, defs).replace('./', '')
|
|
simcoll = self.dirs.get(sim_dir_path)
|
|
if simcoll is None:
|
|
simcoll = SC.SimCollection(sim_dir)
|
|
self.dirs[sim_dir_path] = simcoll
|
|
|
|
sim = simcoll.get_simulator(sim_name, sim_dir, sim_dir_path, test_name, buildrom)
|
|
|
|
# Remove compile command line elements and do one level of variable expansion, split the resulting
|
|
# string into a list.
|
|
all_comps = []
|
|
for comp in [ SPM.normalize_variables(act) for act in compile_action.split() if not self.compile_elems_to_ignore(act) ]:
|
|
all_comps.extend(comp.split())
|
|
|
|
if debug >= 3:
|
|
print('{0}all_comps after filtering:'.format(depth))
|
|
pprint.pp(all_comps)
|
|
|
|
# Iterate through the final compile component list and extract source files, includes
|
|
# and defines.
|
|
#
|
|
# Deferred: Looking for options that set the i64, z64, video library options.
|
|
while all_comps:
|
|
comp = all_comps[0]
|
|
# print(':: comp {0}'.format(comp))
|
|
if self.is_source(comp):
|
|
# Source file...
|
|
## sim.add_source(comp.replace(sim_dir + '/', ''))
|
|
sim.add_source(comp)
|
|
elif comp.startswith('-I'):
|
|
all_comps = self.process_flag(all_comps, defs, sim.add_include, depth)
|
|
elif comp.startswith('-D'):
|
|
all_comps = self.process_flag(all_comps, defs, sim.add_define, depth)
|
|
elif comp.startswith('-L') or comp.startswith('-l'):
|
|
## It's a library path or library. Skip.
|
|
pass
|
|
else:
|
|
# Solitary variable expansion?
|
|
m = SPM._var_rx.match(comp)
|
|
if m:
|
|
varname = m.group(1)
|
|
if varname not in SC._special_vars:
|
|
if not self.is_source_macro(comp, defs):
|
|
expand = [ SPM.normalize_variables(elem) for elem in SPM.shallow_expand_vars(comp, defs).split()
|
|
if not self.source_elems_to_ignore(elem) ]
|
|
SU.emit_debug(debug, 3, '{0}var expanded {1} -> {2}'.format(depth, comp, expand))
|
|
all_comps[1:] = expand + all_comps[1:]
|
|
else:
|
|
## Source macro
|
|
self.collect_source_macros(comp, defs, varname, simcoll, sim, debug, depth)
|
|
else:
|
|
sim.add_source(comp)
|
|
else:
|
|
# Nope.
|
|
print('{0}unknown component: {1}'.format(depth, comp))
|
|
all_comps = all_comps[1:]
|
|
|
|
sim.scan_for_flags(defs)
|
|
sim.cleanup_defines()
|
|
|
|
if debug >= 2:
|
|
pprint.pprint(sim)
|
|
|
|
|
|
def compile_elems_to_ignore(self, elem):
|
|
return (elem in self._ignore_compile_elems or elem.endswith('_LDFLAGS'))
|
|
|
|
def source_elems_to_ignore(self, elem):
|
|
return self.compile_elems_to_ignore(elem) or elem in _special_sources
|
|
|
|
|
|
def is_source_macro(self, var, defs):
|
|
"""Is the macro/variable a list of sources?
|
|
"""
|
|
expanded = SPM.expand_vars(var, defs).split()
|
|
# print('is_source_macro {}'.format(expanded))
|
|
return all(map(lambda src: self.is_source(src), expanded))
|
|
|
|
|
|
def is_source(self, thing):
|
|
return thing.endswith('.c')
|
|
|
|
|
|
def process_flag(self, comps, defs, process_func, depth):
|
|
if len(comps[0]) > 2:
|
|
# "-Ddef"
|
|
val = comps[0][2:]
|
|
else:
|
|
# "-D def"
|
|
val = comps[1]
|
|
comps = comps[1:]
|
|
m = SPM._var_rx.match(val)
|
|
if m:
|
|
var = m.group(1)
|
|
## Gracefully deal with undefined variables (ATT3B2M400B2D is a good example)
|
|
if var in defs:
|
|
if var not in self.vars:
|
|
self.vars[var] = defs[var]
|
|
else:
|
|
print('{0}undefined make macro: {1}'.format(depth, var))
|
|
|
|
process_func(val)
|
|
return comps
|
|
|
|
|
|
def collect_vars(self, defs, debug=0):
|
|
"""Add indirectly referenced macros and variables, adding them to the defines dictionary.
|
|
|
|
Indirectly referenced macros and variables are macros and variables embedded in existing
|
|
variables, source macros and include lists. For example, SIMHD is an indirect reference
|
|
in "KA10D = ${SIMHD}/ka10" because KA10D might never have been expanded by 'extract()'.
|
|
"""
|
|
|
|
def scan_var(varset, var):
|
|
tmp = var
|
|
return varset.union(set(SPM.extract_variables(tmp)))
|
|
|
|
def replace_simhd(l, v):
|
|
l.append(v.replace('SIMHD', 'CMAKE_SOURCE_DIR'))
|
|
return l
|
|
|
|
simvars = set()
|
|
for v in self.vars.values():
|
|
if isinstance(v, list):
|
|
simvars = functools.reduce(scan_var, v, simvars)
|
|
else:
|
|
simvars = scan_var(simvars, v)
|
|
|
|
for dir in self.dirs.keys():
|
|
simvars = simvars.union(self.dirs[dir].get_simulator_vars(debug))
|
|
|
|
if debug >= 2:
|
|
print('Collected simvars:')
|
|
pprint.pprint(simvars)
|
|
|
|
for var in simvars:
|
|
if var not in self.vars:
|
|
if var in defs:
|
|
self.vars[var] = defs[var]
|
|
else:
|
|
print('{0}: variable not defined.'.format(var))
|
|
|
|
## Replace SIMHD with CMAKE_SOURCE_DIR
|
|
for k, v in self.vars.items():
|
|
if isinstance(v, list):
|
|
v = functools.reduce(replace_simhd, v, [])
|
|
else:
|
|
v = v.replace('SIMHD', 'CMAKE_SOURCE_DIR')
|
|
self.vars[k] = v
|
|
|
|
def collect_source_macros(self, comp, defs, varname, simcoll, sim, debug=0, depth=''):
|
|
def inner_sources(srcmacro):
|
|
for v in srcmacro:
|
|
if not self.source_elems_to_ignore(v):
|
|
m = SPM._var_rx.match(v)
|
|
if m is not None:
|
|
vname = m.group(1)
|
|
vardef = defs.get(vname)
|
|
if vardef is not None:
|
|
## Convert the macro variable into a list
|
|
vardef = [ SPM.normalize_variables(v) \
|
|
for v in vardef.split() if not self.source_elems_to_ignore(v) ]
|
|
SU.emit_debug(debug, 3, '{0}source macro: {1} -> {2}'.format(depth, vname, vardef))
|
|
simcoll.add_source_macro(vname, vardef, sim)
|
|
## Continue into the macro variable's definitions
|
|
inner_sources(vardef)
|
|
|
|
vardef = defs.get(varname)
|
|
if vardef is not None:
|
|
vardef = [ SPM.normalize_variables(v) \
|
|
for v in vardef.split() if not self.source_elems_to_ignore(v) ]
|
|
SU.emit_debug(debug, 3, '{0}source macro: {1} -> {2}'.format(depth, varname, vardef))
|
|
simcoll.add_source_macro(varname, vardef, sim)
|
|
inner_sources(vardef)
|
|
SU.emit_debug(debug, 3, '{0}source added: {1}'.format(depth, comp))
|
|
sim.add_source(comp)
|
|
else:
|
|
print('{0}undefined make macro: {1}'.format(depth, varname))
|
|
|
|
def write_vars(self, stream):
|
|
def collect_vars(varlist, var):
|
|
varlist.extend(SPM.extract_variables(var))
|
|
return varlist
|
|
|
|
varnames = list(self.vars.keys())
|
|
namewidth = max(map(lambda s: len(s), varnames))
|
|
# vardeps maps the parent variable to its dependents, e.g.,
|
|
# INTELSYSD -> [ISYS8010D, ...]
|
|
vardeps = dict()
|
|
# alldeps is the set of all parent and dependents, which will be
|
|
# deleted from a copy of self.vars. The copy has variables that
|
|
# don't depend on anything (except for CMAKE_SOURCE_DIR, but we
|
|
# know that's defined by CMake.)
|
|
alldeps = set()
|
|
for var in varnames:
|
|
if isinstance(self.vars[var], list):
|
|
mvars = functools.reduce(collect_vars, self.vars[var], [])
|
|
else:
|
|
mvars = SPM.extract_variables(self.vars[var])
|
|
mvars = [mvar for mvar in mvars if mvar != "CMAKE_SOURCE_DIR"]
|
|
if mvars:
|
|
alldeps.add(var)
|
|
for mvar in mvars:
|
|
if mvar not in vardeps:
|
|
vardeps[mvar] = []
|
|
vardeps[mvar].append(var)
|
|
alldeps.add(mvar)
|
|
|
|
nodeps = self.vars.copy()
|
|
## SIMHD will never be used.
|
|
if 'SIMHD' in nodeps:
|
|
del nodeps['SIMHD']
|
|
for dep in alldeps:
|
|
del nodeps[dep]
|
|
|
|
varnames = list(nodeps.keys())
|
|
varnames.sort()
|
|
for var in varnames:
|
|
self.emit_value(var, self.vars[var], stream, namewidth)
|
|
|
|
## Now to emit the dependencies
|
|
depnames = list(vardeps.keys())
|
|
depnames.sort()
|
|
for dep in depnames:
|
|
self.write_dep(dep, vardeps, alldeps, namewidth, stream)
|
|
|
|
## stream.write('\n## ' + '-' * 40 + '\n')
|
|
|
|
def write_dep(self, dep, vardeps, alldeps, width, stream):
|
|
# Not the most efficient, but it works
|
|
alldeps.discard(dep)
|
|
for parent in [ v for v in vardeps.keys() if dep in vardeps[v]]:
|
|
if dep in parent.values():
|
|
self.write_dep(parent, vardeps, alldeps, width, stream)
|
|
|
|
stream.write('\n')
|
|
self.emit_value(dep, self.vars[dep], stream, width)
|
|
|
|
children = vardeps[dep]
|
|
children.sort()
|
|
for child in children:
|
|
if child in alldeps:
|
|
self.emit_value(child, self.vars[child], stream, width)
|
|
alldeps -= set(children)
|
|
|
|
def emit_value(self, var, value, stream, width=0):
|
|
if isinstance(value, list):
|
|
stream.write('set({:{width}} {})\n'.format(var, ' '.join(map(lambda s: '"' + s + '"', value)),
|
|
width=width))
|
|
else:
|
|
stream.write('set({:{width}} "{}")\n'.format(var, value, width=width))
|
|
|
|
def write_simulators(self, toplevel_dir, debug=0):
|
|
dirnames = list(self.dirs.keys())
|
|
dirnames.sort()
|
|
|
|
for subdir in dirnames:
|
|
simcoll = self.dirs[subdir]
|
|
test_label = subdir
|
|
# Group tests under subdirectories together
|
|
has_slash = test_label.find('/')
|
|
if has_slash < 0:
|
|
has_slash = test_label.find('\\')
|
|
if has_slash >= 0:
|
|
test_label = test_label[:has_slash]
|
|
## Write out individual CMakeLists.txt:
|
|
subdir_cmake = os.path.join(toplevel_dir, subdir, 'CMakeLists.txt')
|
|
print('==== writing to {0}'.format(subdir_cmake))
|
|
with open(subdir_cmake, "w", newline='\r\n') as stream2:
|
|
plural = '' if len(self.dirs[subdir]) == 1 else 's'
|
|
stream2.write('## {} simulator{plural}\n'.format(subdir, plural=plural))
|
|
stream2.write('\n'.join(_individual_header))
|
|
stream2.write('\n')
|
|
stream2.write('if (HAVE_UNITY_FRAMEWORK AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/unit-tests/CMakeLists.txt")\n')
|
|
stream2.write(' add_subdirectory(unit-tests)\n')
|
|
stream2.write('endif ()')
|
|
stream2.write('\n')
|
|
simcoll.write_simulators(stream2, debug=debug, test_label=test_label)
|
|
|
|
## Write out unit tests, if the unit test subdirectory exists:
|
|
subdir_units_dir = os.path.join(toplevel_dir, subdir, 'unit-tests')
|
|
subdir_units = os.path.join(subdir_units_dir, 'CMakeLists.txt')
|
|
if os.path.exists(subdir_units):
|
|
with open(subdir_units, "w") as stream2:
|
|
plural = '' if len(self.dirs[subdir]) == 1 else 's'
|
|
stream2.write('## {} simulator{plural}\n'.format(subdir, plural=plural))
|
|
stream2.write('\n'.join(_unit_test_header))
|
|
stream2.write('\n')
|
|
simcoll.write_unit_tests(stream2, debug, subdir)
|
|
|
|
simh_subdirs = os.path.join(toplevel_dir, 'cmake', 'simh-simulators.cmake')
|
|
print("==== writing {0}".format(simh_subdirs))
|
|
with open(simh_subdirs, "w") as stream2:
|
|
stream2.write('\n'.join(_individual_header))
|
|
self.write_vars(stream2)
|
|
stream2.write('\n## ' + '-' * 40 + '\n')
|
|
stream2.write(_unset_display_vars)
|
|
stream2.write('\n## ' + '-' * 40 + '\n\n')
|
|
stmts = [ 'add_subdirectory(' + dir + ')' for dir in dirnames ]
|
|
stream2.write('\n'.join(stmts))
|
|
stream2.write('\n')
|
|
|
|
## Representation when printed
|
|
def __repr__(self):
|
|
return '{0}({1}, {2})'.format(self.__class__.__name__, self.dirs.__repr__(), self.vars.__repr__())
|
|
|
|
|
|
if '_dispatch' in pprint.PrettyPrinter.__dict__:
|
|
def cmake_pprinter(pprinter, cmake, stream, indent, allowance, context, level):
|
|
cls = cmake.__class__
|
|
stream.write(cls.__name__ + '(')
|
|
indent += len(cls.__name__) + 1
|
|
pprinter._format(cmake.dirs, stream, indent, allowance + 2, context, level)
|
|
stream.write(',\n' + ' ' * indent)
|
|
pprinter._format(cmake.vars, stream, indent, allowance + 2, context, level)
|
|
stream.write(')')
|
|
|
|
pprint.PrettyPrinter._dispatch[CMakeBuildSystem.__repr__] = cmake_pprinter
|