Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Factor out some file-generation machinery #181

Merged
merged 9 commits into from
May 17, 2024
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()))
30 changes: 30 additions & 0 deletions config/query_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
import argparse, os, subprocess

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_version():
version_file = os.path.join(os.path.dirname(__file__), '../VERSION')
return _call("tail -1 " + version_file)
mabruzzo marked this conversation as resolved.
Show resolved Hide resolved

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
2 changes: 1 addition & 1 deletion src/clib/Make.config.assemble
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,4 @@
# LIBRARY RELEASE VERSION
#-----------------------------------------------------------------------

LIB_RELEASE_VERSION = 3.3.dev1
LIB_RELEASE_VERSION := $(shell $(QUERY_VERSION) show-version)
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
73 changes: 27 additions & 46 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 @@ -198,56 +200,35 @@ verbose: VERBOSE = 1
#-----------------------------------------------------------------------

.PHONY: autogen
autogen: config_type auto_show_config.c auto_show_flags.c auto_get_version.c
autogen: config_type auto_general.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

# Force update of auto_show_config.c
config_type: grackle_float.h.in
@(if [ $(ASSEMBLE_PRECISION_NUMBER) -eq 4 ]; then \
$(CONFIG_DIR)/configure_file.py --clobber \
--input grackle_float.h.in \
--output grackle_float.h \
GRACKLE_FLOAT_MACRO=GRACKLE_FLOAT_4; \
else \
$(CONFIG_DIR)/configure_file.py --clobber \
--input grackle_float.h.in \
--output grackle_float.h \
GRACKLE_FLOAT_MACRO=GRACKLE_FLOAT_8; \
fi)
mabruzzo marked this conversation as resolved.
Show resolved Hide resolved

.PHONY: auto_show_config.c
auto_show_config.c:
# 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 +288,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
18 changes: 18 additions & 0 deletions src/clib/auto_general.c.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include <stdio.h>
#include "grackle.h"

grackle_version get_grackle_version(void) {
grackle_version out;
out.version = "@VERSION_NUM@";
out.branch = "@GIT_BRANCH@";
out.revision = "@GIT_REVISION@";
return out;
}

void auto_show_flags(FILE *fp) {
fprintf (fp, "%s\n", "@SHOW_FLAGS_STR@");
}

void auto_show_config(FILE *fp) {
fprintf (fp, "%s\n", "@SHOW_CONFIG_STR@");
}
4 changes: 4 additions & 0 deletions src/clib/grackle_float.h.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#ifndef __GRACKLE_FLOAT_H__
#define __GRACKLE_FLOAT_H__
#define @GRACKLE_FLOAT_MACRO@
#endif