From cb94fb27590e24b15e779edd06d7ba01c80c8064 Mon Sep 17 00:00:00 2001 From: Simon Rose <simon.rose@ess.eu> Date: Thu, 28 Oct 2021 15:11:16 +0200 Subject: [PATCH] Replace expandDBD.tcl with python script The purpose of this is to remove the dependency on tclx, a non-standard package. --- CHANGELOG.md | 8 +++ configure/module/RULES_REQUIRE | 2 +- require-ess/tools/driver.makefile | 4 +- require-ess/tools/expandDBD | 103 ++++++++++++++++++++++++++ require-ess/tools/expandDBD.tcl | 115 ------------------------------ tests/test_expand_dbd.py | 56 +++++++++++---- 6 files changed, 157 insertions(+), 131 deletions(-) create mode 100755 require-ess/tools/expandDBD delete mode 100755 require-ess/tools/expandDBD.tcl diff --git a/CHANGELOG.md b/CHANGELOG.md index e5508b50..3581d885 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### New Features + +### Bugfixes + +### Other changes + +* Replaced `tclx` script to expand .dbd files with a python script + ## [5.0.0] ### New Features diff --git a/configure/module/RULES_REQUIRE b/configure/module/RULES_REQUIRE index 2370898e..22cd10d5 100644 --- a/configure/module/RULES_REQUIRE +++ b/configure/module/RULES_REQUIRE @@ -23,7 +23,7 @@ endif install: requireconf requireconf: e3-site-path e3-require-path - $(QUIET) install -m 755 $(wildcard $(E3_MODULE_SRC_PATH)/tools/*.tcl) $(E3_REQUIRE_TOOLS)/ + $(QUIET) install -m 755 $(wildcard $(E3_MODULE_SRC_PATH)/tools/expandDBD) $(E3_REQUIRE_TOOLS)/ $(QUIET) install -m 644 $(E3_MODULE_SRC_PATH)/tools/driver.makefile $(E3_REQUIRE_TOOLS)/ $(QUIET) install -m 755 $(E3_MODULE_SRC_PATH)/tools/revision_number $(E3_REQUIRE_TOOLS)/ $(QUIET) install -m 755 $(E3_SHELL_FILES) $(E3_REQUIRE_BIN)/ diff --git a/require-ess/tools/driver.makefile b/require-ess/tools/driver.makefile index ceab7598..bf7c6a6c 100644 --- a/require-ess/tools/driver.makefile +++ b/require-ess/tools/driver.makefile @@ -216,7 +216,7 @@ export DBD_SRCS #record dbd files given in DBDS RECORDS1 = $(patsubst %Record.dbd, %, $(filter-out dev%, $(filter %Record.dbd, $(notdir ${DBD_SRCS})))) #record dbd files included by files given in DBDS -RECORDS2 = $(filter-out dev%, $(shell ${MAKEHOME}/expandDBD.tcl -r $(addprefix -I, $(sort $(dir ${DBD_SRCS}))) $(realpath ${DBDS}))) +RECORDS2 = $(filter-out dev%, $(shell ${MAKEHOME}/expandDBD -r $(addprefix -I, $(sort $(dir ${DBD_SRCS}))) $(realpath ${DBDS}) 2> /dev/null)) RECORDS = $(sort ${RECORDS1} ${RECORDS2}) export RECORDS @@ -610,7 +610,7 @@ MODULEINFOS: # because it has too strict checks to be used for a loadable module. ${MODULEDBD}: ${DBDFILES} @echo "Expanding $@" - ${MAKEHOME}expandDBD.tcl -$(basename ${EPICSVERSION}) ${DBDEXPANDPATH} $^ > $@ + ${MAKEHOME}expandDBD ${DBDEXPANDPATH} $^ > $@ # Install everything. INSTALL_LIBS = ${MODULELIB:%=${INSTALL_LIB}/%} diff --git a/require-ess/tools/expandDBD b/require-ess/tools/expandDBD new file mode 100755 index 00000000..9e894f5b --- /dev/null +++ b/require-ess/tools/expandDBD @@ -0,0 +1,103 @@ +#!/usr/bin/env python +from __future__ import print_function + +import argparse +import os +import re +import sys + +SCANNED_FILES = set() +RECORD_REGEX = re.compile(r"include[ \t]*\"?((.*)Record.dbd)\"?") +INCLUDE_REGEX = re.compile(r"include[ \t]+\"?([^\"]*)\"?") +BLANK_LINE_REGEX = re.compile(r"^[ \t]*(#|%|$)") + + +def find_dbd_file(filename, include_paths): + """Search for a given .dbd file among the include paths.""" + for include_path in include_paths: + potential_path = os.path.join(include_path, filename) + if os.path.exists(potential_path): + return potential_path + return None + + +def open_dbd_file(current_file, filename, includes=None): + """Open a .dbd file and return the lines.""" + if not includes: + includes = ["."] + + basename = os.path.basename(filename) + if basename in SCANNED_FILES: + print( + "Info: skipping duplicate file {basename} included from {current_file}".format( + basename=basename, current_file=current_file + ), + file=sys.stderr, + ) + return [] + dbd_file = find_dbd_file(basename, includes) + if dbd_file is None: + print("File '{basename}' not found".format(basename=basename), file=sys.stderr) + sys.exit(1) + SCANNED_FILES.add(basename) + with open(dbd_file, "r") as f: + return [line.strip() for line in f.readlines()] + + +def expand_dbd_file(current_file, dbdlines, includes): + """Print to stdout a fully-expanded .dbd file.""" + for line in dbdlines: + if BLANK_LINE_REGEX.match(line): + continue + m = INCLUDE_REGEX.search(line) + if m: + lines = open_dbd_file(current_file, m.group(1), includes) + expand_dbd_file(m.group(1), lines, includes) + else: + print(line) + + +def parse_record_dbd_file(current_file, dbdlines, includes): + """Recursively search through .dbd files for xRecord.dbd, and print the record names.""" + for line in dbdlines: + if BLANK_LINE_REGEX.match(line): + continue + m = RECORD_REGEX.search(line) + if m: + if find_dbd_file(m.group(1), includes): + print(m.group(2)) + else: + print( + "File '{group}' not found".format(group=m.group(1)), file=sys.stderr + ) + sys.exit(1) + continue + m = INCLUDE_REGEX.search(line) + if m: + lines = open_dbd_file(current_file, m.group(1), includes) + parse_record_dbd_file(m.group(1), lines, includes) + + +def handle_dbd_file(dbdfiles, include, record_types): + """Process the .dbd files.""" + for dbdfile in dbdfiles: + lines = open_dbd_file("command line", dbdfile, include) + if record_types: + parse_record_dbd_file(dbdfile, lines, include) + else: + expand_dbd_file(dbdfile, lines, include) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-I", "--include", action="append") + parser.add_argument("-r", "--record-types", action="store_true") + parser.add_argument("dbdfiles", nargs="+") + + args = parser.parse_args() + + handle_dbd_file(**vars(args)) + + +if __name__ == "__main__": + main() diff --git a/require-ess/tools/expandDBD.tcl b/require-ess/tools/expandDBD.tcl deleted file mode 100755 index 3cd4bc78..00000000 --- a/require-ess/tools/expandDBD.tcl +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env tclsh - -package require Tclx - -set global_context [scancontext create] - -set epicsversion 3.14 -set quiet 0 -set recordtypes 0 -set seachpath {} -set filesDone {} - -while {[llength $argv]} { - switch -glob -- [lindex $argv 0] { - "-[0-9]*" { set epicsversion [string range [lindex $argv 0] 1 end]} - "-q" { set quiet 1 } - "-r" { set recordtypes 1; set quiet 1 } - "-I" { lappend seachpath [lindex $argv 1]; set argv [lreplace $argv 0 1]; continue } - "-I*" { lappend seachpath [string range [lindex $argv 0] 2 end] } - "--" { set argv [lreplace $argv 0 0]; break } - "-*" { puts stderr "Warning: Unknown option [lindex $argv 0] ignored" } - default { break } - } - set argv [lreplace $argv 0 0] -} - -proc opendbd {name} { - global seachpath - foreach dir $seachpath { - if ![catch { - set file [open [file join $dir $name]] - }] { - return $file - } - } - return -code error "file $name not found" -} - -scanmatch $global_context {^[ \t]*(#|%|$)} { - continue -} - -if {$recordtypes} { - scanmatch $global_context {include[ \t]+"?((.*)Record.dbd)"?} { - if ![catch { - close [opendbd $matchInfo(submatch0)] - }] { - puts $matchInfo(submatch1) - } - continue -} - -} else { - - scanmatch $global_context {(registrar|variable|function)[ \t]*\([ \t]*"?([a-zA-Z0-9_]+)"?[ \t]*\)} { - global epicsversion - if {$epicsversion > 3.13} {puts $matchInfo(submatch0)($matchInfo(submatch1))} - } - scanmatch $global_context {variable[ \t]*\([ \t]*"?([a-zA-Z0-9_]+)"?[ \t]*,[ \t]*"?([a-zA-Z0-9_]+)"?[ \t]*\)} { - global epicsversion - if {$epicsversion > 3.13} {puts variable($matchInfo(submatch0),$matchInfo(submatch1))} - } - - scanmatch $global_context { - puts $matchInfo(line) - } -} - -scanmatch $global_context {include[ \t]+"?([^"]*)"?} { - global seachpath - global FileName - global quiet - if [catch { - includeFile $global_context $matchInfo(submatch0) - } msg] { - if {!$quiet} { - puts stderr "ERROR: $msg in path \"$seachpath\" called from $FileName($matchInfo(handle)) line $matchInfo(linenum)" - exit 1 - } - } - continue -} - -proc includeFile {context filename} { - global global_context FileName filesDone matchInfo quiet - set basename [file tail $filename] - if {[lsearch $filesDone $basename ] != -1} { - if {!$quiet} { - puts stderr "Info: skipping duplicate file $basename included from $FileName($matchInfo(handle))" - } - return - } - if {$filename != "dbCommon.dbd"} { lappend filesDone [file tail $filename] } - set file [opendbd $filename] - set FileName($file) $filename - #puts "#include $filename from $FileName($matchInfo(handle))" - scanfile $context $file - close $file -} - -foreach filename $argv { - global filesDone quiet - set basename [file tail $filename] - if {[lsearch $filesDone $basename] != -1} { - if {!$quiet} { - puts stderr "Info: skipping duplicate file $basename from command line" - } - continue - } - if {$basename != "dbCommon.dbd"} { lappend filesDone $basename } - set file [open $filename] - set FileName($file) $filename - scanfile $global_context $file - close $file -} diff --git a/tests/test_expand_dbd.py b/tests/test_expand_dbd.py index eae44dee..f1384d6d 100644 --- a/tests/test_expand_dbd.py +++ b/tests/test_expand_dbd.py @@ -8,33 +8,33 @@ import pytest @pytest.fixture def expanddbdtcl(): require_path = Path(os.environ.get("E3_REQUIRE_LOCATION")) - expanddbdtcl = require_path / "tools" / "expandDBD.tcl" + expanddbdtcl = require_path / "tools" / "expandDBD" assert expanddbdtcl.is_file() and os.access(expanddbdtcl, os.X_OK) return expanddbdtcl -def test_missing_dbd_file(tmpdir, expanddbdtcl): - dbd_file = Path(tmpdir) / "tmp.dbd" +def test_missing_dbd_file(tmp_path, expanddbdtcl): + dbd_file = tmp_path / "tmp.dbd" dbd_file.write_text("include not_a_file.dbd") result = subprocess.run( - [expanddbdtcl, dbd_file], + [expanddbdtcl, "-I", tmp_path, dbd_file], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", ) assert result.returncode == 1 - assert "file not_a_file.dbd not found" in result.stderr + assert "File 'not_a_file.dbd' not found" in result.stderr -def test_include_dbd_file(tmpdir, expanddbdtcl): - dbd_a = Path(tmpdir) / "a.dbd" +def test_include_dbd_file(tmp_path, expanddbdtcl): + dbd_a = tmp_path / "a.dbd" dbd_a.write_text("include b.dbd") - dbd_b = Path(tmpdir) / "b.dbd" + dbd_b = tmp_path / "b.dbd" dbd_b.write_text("content") result = subprocess.run( - [expanddbdtcl, "-I", str(tmpdir), dbd_a], + [expanddbdtcl, "-I", str(tmp_path), dbd_a], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", @@ -43,18 +43,48 @@ def test_include_dbd_file(tmpdir, expanddbdtcl): assert "content" == result.stdout.strip("\n") -def test_skip_repeated_includes(tmpdir, expanddbdtcl): - dbd_a = Path(tmpdir) / "a.dbd" +def test_skip_repeated_includes(tmp_path, expanddbdtcl): + dbd_a = tmp_path / "a.dbd" dbd_a.write_text("include b.dbd\ninclude b.dbd") - dbd_b = Path(tmpdir) / "b.dbd" + dbd_b = tmp_path / "b.dbd" dbd_b.touch() result = subprocess.run( - [expanddbdtcl, "-I", str(tmpdir), dbd_a], + [expanddbdtcl, "-I", str(tmp_path), dbd_a], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", ) assert result.returncode == 0 assert "Info: skipping duplicate file b.dbd included from" in result.stderr + + +def test_record_names_from_dbds(tmp_path, expanddbdtcl): + dbd_a = tmp_path / "a.dbd" + dbd_a.write_text("include aRecord.dbd") + + dbd_record = tmp_path / "aRecord.dbd" + dbd_record.touch() + + result = subprocess.run( + [expanddbdtcl, "-r", "-I", str(tmp_path), dbd_a], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert result.returncode == 0 + assert result.stdout.splitlines() == ["a"] + + +def test_missing_record_names_from_dbds(tmp_path, expanddbdtcl): + dbd_a = tmp_path / "a.dbd" + dbd_a.write_text("include aRecord.dbd") + + result = subprocess.run( + [expanddbdtcl, "-r", "-I", str(tmp_path), dbd_a], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert result.returncode == 1 -- GitLab