Skip to content

Commit

Permalink
Merge pull request #181 from mabruzzo/refactoring-autogen
Browse files Browse the repository at this point in the history
Factor out some file-generation machinery
  • Loading branch information
brittonsmith committed May 17, 2024
2 parents dc90468 + 061f3be commit 58dcca2
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 67 deletions.
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.3.dev1
170 changes: 170 additions & 0 deletions config/configure_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#!/usr/bin/env python3

# for the sake of portability, we try to avoid fstrings

import argparse
import os
import re
import string
import sys

_MAX_VARNAME_SIZE = 256
_VALID_VARNAME_STR = '\\w{{1,{}}}'.format(_MAX_VARNAME_SIZE)
_PATTERN = re.compile(r'(@{}@)|(@[^\s@]*@?)'.format(_VALID_VARNAME_STR))
_ERR_MSG_TEMPLATE = (
"{!r}, the string starting with occurence number {} of the '@' character "
"on line number {} doesn't specify a valid variable name. "
"A valid variable name is string enclosed by 2 '@' symbolds, where the "
"string and is composed of 1 to {} alphanumeric ASCII characters. An "
"alphanumeric character is an uppercase or lowercase letter (A-Z or a-z), "
"a digit (0-9) or an underscore (_)")

def is_valid_varname(s, start = None, stop = None):
return re.fullmatch(_VALID_VARNAME_STR, s[slice(start, stop)]) is not None


def configure_file(lines, variable_map, out_fname):
"""
Writes a new file to out_fname, line-by-line, while performing variable
substituions
"""

used_variable_set = set()
out_f = open(out_fname, 'w')
err_msg = None

def replace(matchobj):
nonlocal err_msg, match_count, used_variable_set, variable_map
if matchobj.lastindex == 1:
varname = matchobj[1][1:-1]
if varname in variable_map:
match_count += 1
used_variable_set.add(varname)
return variable_map[varname]
err_msg = ("the variable {} (specified by a string enclosed by a "
"pair of '@' characters on line {}) doesn't have an "
"associated value").format(varname, line_num)
elif err_msg is None:
err_msg = _ERR_MSG_TEMPLATE.format(
matchobj[0], 2*match_count+1, line_num, _MAX_VARNAME_SIZE)
return '-' # denotes bad case

for line_num, line in enumerate(lines):
# make sure to drop any trailing '\n'
assert line[-1] == '\n', "sanity check!"
line = line[:-1]
match_count = 0

out_f.write(_PATTERN.sub(replace,line))
out_f.write('\n')
if err_msg is not None:
out_f.close()
os.remove(out_fname)
raise RuntimeError(rslt)

unused_variables = used_variable_set.symmetric_difference(variable_map)

if len(unused_variables) > 0:
os.remove(out_fname)
raise RuntimeError("the following variable(s) were specified, but "
"were unused: {!r}".format(unused_variables))

def _parse_variables(dict_to_update, var_val_assignment_str_l,
val_is_file_path = False):
for var_val_assignment_str in var_val_assignment_str_l:
stripped_str = var_val_assignment_str.strip() # for safety

# so the the contents should look like "<VAR>=<VAL>"
# - For now, we don't tolerate any whitespace.
# - If we revisit this choice:
# - we should be mindful of what it would take to actually escape
# whitespace on the command line.
# - Doing so often involves quotation marks, that are consumed by the
# shell-parsing. It might not be intuitive how such quotation marks
# affect the output of this script.
# - Consquently it may be more trouble than it's worth to support
# whitespace

for character in (string.whitespace + '@'):
if character in stripped_str:
raise RuntimeError(
("a variable-value pair, must not contain the '{}' "
"character. The character is present in {!r}").format(
character, stripped_str))
if stripped_str.count('=') != 1:
raise RuntimeError(
"each variable-value pair, must contain exactly 1 '=' "
"charater. This isn't true for {!r}".format(stripped_str))
var_name, value = stripped_str.split('=')

if not is_valid_varname(var_name):
raise RuntimeError(
"{!r} is not a valid variable name".format(var_name))
elif var_name in dict_to_update:
raise RuntimeError(
"the {!r} variable is defined more than once".format(var_name))

if val_is_file_path:
path = value
if not os.path.isfile(path):
raise RuntimeError(
("error while trying to associate the contents of the file "
"at {!r} with the {!r} variable: no such file exists"
).format(path, var_name))
with open(value, "r") as f:
# we generally treat the characters in the file as literals
# -> we do need to make a point of properly escaping the
# newline characters
assert os.linesep == '\n' # implicit assumption
value = f.read().replace(os.linesep, r'\n')
dict_to_update[var_name] = value

def main(args):
# handle clobber-related logic
clobber, out_fname = args.clobber, args.output
if (os.path.isfile(out_fname) and not clobber):
raise RuntimeError(
("A file already exists at {!r}. To overwrite use the --clobber "
"flag").format(out_fname))

# fill variable_map with the specified variables and values
variable_map = {}
_parse_variables(variable_map, args.variables,
val_is_file_path = False)
_parse_variables(variable_map, args.variable_use_file_contents,
val_is_file_path = True)

# use variable_map to actually create the output file
with open(args.input, 'r') as f_input:
line_iterator = iter(f_input)
configure_file(lines = line_iterator,
variable_map = variable_map,
out_fname = out_fname)

return 0

parser = argparse.ArgumentParser(description='Configure template files.')
parser.add_argument(
'--variable-use-file-contents', action = 'append', default = [],
metavar = 'VAR=path/to/file',
help = ("associates the (possibly multi-line) contents contained by the "
"specified file with VAR")
)
parser.add_argument(
"variables", nargs = '*', action = 'store', default = [],
metavar = 'VAR=VAL',
help = ("associates the value, VAL, with the specified variable, VAR")
)
parser.add_argument(
"-i", "--input", required = True, help = "path to input template file"
)
parser.add_argument(
"-o", "--output", required = True, help = "path to output template file"
)
parser.add_argument(
"--clobber", action = "store_true",
help = "overwrite the output file if it already exists"
)

if __name__ == '__main__':
sys.exit(main(parser.parse_args()))
38 changes: 38 additions & 0 deletions config/query_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
import argparse, os, subprocess

def get_last_line(path):
last_line = None
with open(path, 'r') as f:
for line in filter(lambda l: len(l) > 0 and not l.isspace(), f):
last_line = line
if last_line is None:
raise ValueError("the {} file is empty".format(path))
return last_line.rstrip()

def query_version():
return get_last_line(os.path.join(os.path.dirname(__file__), '../VERSION'))

def _call(command, **kwargs):
rslt = subprocess.check_output(command, shell = True, **kwargs)
return rslt.decode().rstrip() # return as str & remove any trailing '\n'

def query_git(command):
# note: we explicitly redirect stderr since `&>` is not portable
git_is_installed = _call('command -v git > /dev/null 2>&1 && '
'git status > /dev/null 2>&1 && '
'echo "1"') == "1"
return _call(command) if git_is_installed else "N/A"

choices = {"show-version" : query_version,
"git-branch" : lambda: query_git("git rev-parse --abbrev-ref HEAD"),
"git-revision" : lambda: query_git("git rev-parse HEAD")}

parser = argparse.ArgumentParser("query version information")
parser.add_argument('directive', choices = list(choices),
help = "specifies the information to check")

if __name__ == '__main__':
args = parser.parse_args()
result = choices[args.directive]()
print(result)
8 changes: 6 additions & 2 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))

sys.path.insert(0, os.path.abspath('../../config'))

from query_version import query_version

# -- General configuration -----------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
Expand Down Expand Up @@ -48,9 +52,9 @@
# built documents.
#
# The short X.Y version.
version = '3.3.dev1'
version = query_version()
# The full version, including alpha/beta/rc tags.
release = '3.3.dev1'
release = version

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
6 changes: 0 additions & 6 deletions src/clib/Make.config.assemble
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,3 @@
ifdef MACH_INSTALL_INCLUDE_DIR
INSTALL_INCLUDE_DIR = $(MACH_INSTALL_INCLUDE_DIR)
endif

#-----------------------------------------------------------------------
# LIBRARY RELEASE VERSION
#-----------------------------------------------------------------------

LIB_RELEASE_VERSION = 3.3.dev1
4 changes: 1 addition & 3 deletions src/clib/Make.config.objects
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
#-----------------------------------------------------------------------

OBJS_CONFIG_LIB = \
auto_show_config.lo \
auto_show_flags.lo \
auto_get_version.lo \
auto_general.lo \
calculate_cooling_time.lo \
calculate_dust_temperature.lo \
calculate_gamma.lo \
Expand Down
11 changes: 2 additions & 9 deletions src/clib/Make.config.targets
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,8 @@ suggest-clean:
show-version:
@echo
@echo "The Grackle Version $(LIB_RELEASE_VERSION)"
@USEGIT=`command -v git &> /dev/null && git status &> /dev/null && \
echo "1"`; \
if [ "$${USEGIT}" == "1" ]; then \
echo "Git Branch `git rev-parse --abbrev-ref HEAD`"; \
echo "Git Revision `git rev-parse HEAD`"; \
else \
echo "Git Branch N/A"; \
echo "Git Revision N/A"; \
fi
@echo "Git Branch `$(QUERY_VERSION) git-branch`"
@echo "Git Revision `$(QUERY_VERSION) git-revision`"

#-----------------------------------------------------------------------

Expand Down
79 changes: 32 additions & 47 deletions src/clib/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ OUTPUT = out.compile
VERBOSE = 0
GRACKLE_DIR = .
DEFAULT_INSTALL_PREFIX = /usr/local
CONFIG_DIR = $(GRACKLE_DIR)/../../config
QUERY_VERSION = $(CONFIG_DIR)/query_version.py

#-----------------------------------------------------------------------
# Make.config.settings is used for setting default values to all compile-time
Expand Down Expand Up @@ -86,6 +88,14 @@ include Make.config.assemble

LIBTOOL ?= libtool

# define current library release version
# - previously this was hardcoded to a string within Make.config.assemble
# - now that we started using the QUERY_VERSION script, we need to do it here
# - if Make.config.assemble invoked the script, errors would arise whenever any
# Makefile (like the code-examples Makefile) includes Make.config.assemble,
# without defining the QUERY_VERSION script-path ahead of time
LIB_RELEASE_VERSION := $(shell $(QUERY_VERSION) show-version)

#=======================================================================
# OBJECT FILES
#=======================================================================
Expand Down Expand Up @@ -198,56 +208,31 @@ verbose: VERBOSE = 1
#-----------------------------------------------------------------------

.PHONY: autogen
autogen: config_type auto_show_config.c auto_show_flags.c auto_get_version.c

.PHONY: config_type
config_type:
-@echo "#ifndef __GRACKLE_FLOAT_H__" > grackle_float.h
-@echo "#define __GRACKLE_FLOAT_H__" >> grackle_float.h
-@echo "#define $(PRECISION_DEFINE)" >> grackle_float.h
-@echo "#endif" >> grackle_float.h
autogen: config_type auto_general.c

# Force update of auto_show_config.c

.PHONY: auto_show_config.c
auto_show_config.c:
# in following recipe, GRACKLE_FLOAT_MACRO is set to either GRACKLE_FLOAT_4 or
# GRACKLE_FLOAT_8
.PHONY: config_type
config_type: grackle_float.h.in
@($(CONFIG_DIR)/configure_file.py --clobber \
--input grackle_float.h.in \
--output grackle_float.h \
GRACKLE_FLOAT_MACRO=GRACKLE_FLOAT_$(ASSEMBLE_PRECISION_NUMBER));

# Force update of auto_general.c
.PHONY: auto_general.c
auto_general.c: auto_general.c.in
-@$(MAKE) -s show-config >& temp.show-config
-@awk 'BEGIN {print "#include <stdio.h>\nvoid auto_show_config(FILE *fp) {"}; {print " fprintf (fp,\""$$0"\\n\");"}; END {print "}"}' < temp.show-config > auto_show_config.c

# Force update of auto_show_flags.c

.PHONY: auto_show_flags.c
auto_show_flags.c:
-@$(MAKE) -s show-flags >& temp.show-flags
-@awk 'BEGIN {print "#include <stdio.h>\nvoid auto_show_flags(FILE *fp) {"}; {print " fprintf (fp,\""$$0"\\n\");"}; END {print "}"}' < temp.show-flags > auto_show_flags.c

# Force update of auto_get_version.c

.PHONY: auto_get_version.c
auto_get_version.c:
-@$(MAKE) -s show-version >& temp.show-version
-@(if [ -f $@ ]; then rm $@; fi) # delete the file if it already exists

-@echo '#include <stdio.h>' >> $@
-@echo '#include "grackle_types.h"' >> $@
-@echo '' >> $@
-@echo '// the following macros are auto-generated:' >> $@

-@awk '{ if (NF > 0) { print "#define AUTO_" toupper($$(NF-1)) " \"" $$(NF) "\"" } };' < temp.show-version >> auto_get_version.c

-@echo '' >> $@
-@echo '// test that ensures that all macros were correctly defined:' >> $@
-@echo '#if !(defined(AUTO_VERSION) && defined(AUTO_BRANCH) && defined(AUTO_REVISION))' >> $@
-@echo '#error "Something went wrong while auto-generating macros"' >> $@
-@echo '#endif' >> $@
-@echo '' >> $@
-@echo 'grackle_version get_grackle_version(void) {' >> $@
-@echo ' grackle_version out;' >> $@
-@echo ' out.version = AUTO_VERSION;' >> $@
-@echo ' out.branch = AUTO_BRANCH;' >> $@
-@echo ' out.revision = AUTO_REVISION;' >> $@
-@echo ' return out;' >> $@
-@echo '}' >> $@
@$(CONFIG_DIR)/configure_file.py --clobber \
--input auto_general.c.in \
--output auto_general.c \
--variable-use-file-contents SHOW_FLAGS_STR=./temp.show-flags \
--variable-use-file-contents SHOW_CONFIG_STR=./temp.show-config \
VERSION_NUM=$(LIB_RELEASE_VERSION) \
GIT_BRANCH=`$(QUERY_VERSION) git-branch` \
GIT_REVISION=`$(QUERY_VERSION) git-revision`

#-----------------------------------------------------------------------
# Generate dependency file
Expand Down Expand Up @@ -307,7 +292,7 @@ install:
#-----------------------------------------------------------------------

clean:
-@rm -f *.la .libs/* *.o *.lo DEPEND.bak *~ $(OUTPUT) grackle_float.h *.exe auto_show*.c DEPEND out.make.DEPEND
-@rm -f *.la .libs/* *.o *.lo DEPEND.bak *~ $(OUTPUT) grackle_float.h *.exe auto_*.c temp.show-* DEPEND out.make.DEPEND
-@touch DEPEND

#-----------------------------------------------------------------------
Expand Down

0 comments on commit 58dcca2

Please sign in to comment.