"""Module with utils functions to e3_build_tools."""importenumimportloggingimportpathlibimportreimportsubprocessfromtypingimportDict,Type,Unionimportgit
[docs]defget_wrapper_type(wrapper:Union[git.Repo,pathlib.Path])->WrapperType:"""Check wrapper type. Raises: TypeError: If current directory is not a module. """ifisinstance(wrapper,git.Repo):wrapper=pathlib.Path(str(wrapper.working_dir))ifwrapper.name=="e3-base":returnWrapperType.BASEelifwrapper.name=="e3-require":returnWrapperType.REQUIREelifwrapper.name.startswith("e3-"):returnWrapperType.MODULEraiseTypeError("Not a module, stop.")
[docs]defread_makefile_definitions(content:str)->Dict[str,str]:"""Return the definitions of makefile variables."""config_regex=(r"^\s*(?P<key>[a-zA-Z0-9_]*)\s*[:?]?=\s*(?P<val>[a-zA-Z0-9\.\/\-\_\+]*).*$")config_data={}forlineincontent.split("\n"):m=re.match(config_regex,line)ifm:config_data[m.group("key")]=m.group("val").strip()returnconfig_data
[docs]defmodify_makefile_definitions(content:str,substitutions:Dict[str,str])->str:"""Overwrite the definitions of makefile variables. Declarations that are commented out are ignored. """forvar,valinsubstitutions.items():pattern=rf"^[ \t]*({var}[ \t]*[:?]?=[ \t]*)[a-zA-Z0-9\.\/\-\_\+]*(.*)$"replacement=rf"\g<1>{val}\g<2>"content,changes_made=re.subn(pattern,replacement,content,flags=re.MULTILINE,)ifnotchanges_made:logger.debug(f"Variable {var!r} not found in config file")returncontent
[docs]defmodify_config_file(filename:pathlib.Path,key:str,val:str)->None:"""Modify the data in a config file."""content=filename.read_text()new_content=modify_makefile_definitions(content,{key:val})filename.write_text(new_content)logger.debug(f"Modified config file {filename!s}: substituted {key!r} with {val!r}")
[docs]defcheck_module_version(version:str)->bool:"""Return if module version is in the expected format."""version_pattern=re.compile(r"^(\d+\.){2}\d+((-[\w|.]+)+(\+\d+)?|\+\d+)$")returnversion_pattern.match(version)isnotNone
[docs]defcheck_require_version(version:str)->bool:"""Return if require version is in the expected format."""version_pattern=re.compile(r"^(\d+\.){2}\d+$")returnversion_pattern.match(version)isnotNone
[docs]defcheck_base_version(version:str)->bool:"""Return if base version is in the expected format. Unfortunately epics base does not follow semantic version and sometimes can have one digit more, like on version 7.0.6.1. """version_pattern=re.compile(r"^(\d+\.){2,3}\d+(-\w+)?$")returnversion_pattern.match(version)isnotNone
[docs]defensure_build_number(version:str)->str:"""Return the same string but with `+0` appended if string is a valid version. We have to deal with the implicit addition of +0 by require to modules that have no build number defined, but we only do this for versions of the form "x.y.z". """version_pattern=re.compile(r"^\d+\.\d+\.\d+$")ifversion_pattern.match(version)and"+"notinversion:logger.debug(f"Appending '+0' to version {version!r}")version+="+0"returnversion
[docs]defincrement_build_number(version:str)->str:"""Return a version with incremented build number."""version=ensure_build_number(version)pattern=re.compile(r"([^+]*)\+(.*)")match=pattern.match(version)ifnotmatch:returnversionbuild_number=int(match.group(2))+1new_version=f"{match.group(1)}+{build_number}"logger.debug(f"Incrementing build number {version!r} to {new_version!r}")returnnew_version
[docs]defextract_base_version(base_path:str)->str:"""Return the version as taken from a path to EPICS base. Raises: ValueError: If `base_path` is not a valid EPICS base path. """match=re.search("(?<=base-)[0-9.-]+[a-zA-Z0-9]*",base_path)ifnotmatch:raiseValueErrorreturnmatch.group(0)
[docs]defdeep_merge(first:Type[Dict],second:Type[Dict])->Type[Dict]:"""Return a nested merge of two dictionaries, with `first` taking precedence. Based on https://stackoverflow.com/a/56177639 Each key that is found only in `first` or only in `second` will be included in the output collection with its value intact. For any key in common between `first` and `second`, the corresponding values will be merged, and `first` will take precedence over `second`. """ifnotisinstance(first,dict)ornotisinstance(second,dict):returnsecondiffirstisNoneelsefirstelse:# Compute set of all keys in both dictionaries.keys=set(first.keys())|set(second.keys())# Build output dictionary, merging recursively values with common keys,# where `None` is used to mean the absence of a value.return{key:deep_merge(first.get(key),second.get(key))forkeyinkeys}
[docs]defrun_make(path:pathlib.Path,*makeargs)->subprocess.CompletedProcess:"""Run GNU make in the target directory with the given arguments."""cmd=["make","-C",str(path),*makeargs]logger.debug(f"Running command '{' '.join(cmd)}'")result=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True,encoding="utf-8",)logger.debug(f"stdout: {result.stdout}")logger.debug(f"stderr: {result.stderr}")returnresult