support/package-builder/SpecParser.py
326d5ca8
 # pylint: disable=invalid-name,missing-docstring
2820c61a
 import re
0f1fdc4b
 import platform
2820c61a
 from StringUtils import StringUtils
326d5ca8
 from SpecStructures import rpmMacro, dependentPackageData, Package
45c9260c
 from constants import constants
2820c61a
 
 class SpecParser(object):
343d89e8
 
2820c61a
     def __init__(self):
87815216
         self.cleanMacro = rpmMacro().setName("clean")
         self.prepMacro = rpmMacro().setName("prep")
         self.buildMacro = rpmMacro().setName("build")
         self.installMacro = rpmMacro().setName("install")
         self.changelogMacro = rpmMacro().setName("changelog")
         self.checkMacro = rpmMacro().setName("check")
         self.packages = {}
         self.specAdditionalContent = ""
         self.globalSecurityHardening = ""
         self.defs = {}
5f40784b
         self.conditionalCheckMacroEnabled = False
343d89e8
         self.macro_pattern = re.compile(r'%{(\S+?)\}')
5062126c
 
326d5ca8
     def parseSpecFile(self, specfile):
         self._createDefaultPackage()
         currentPkg = "default"
         with open(specfile) as specFile:
             lines = specFile.readlines()
             totalLines = len(lines)
             i = 0
             while i < totalLines:
                 line = lines[i].strip()
                 if self._isConditionalArch(line):
                     if platform.machine() != self._readConditionalArch(line):
                         # skip conditional body
                         deep = 1
                         while i < totalLines and deep != 0:
                             i = i + 1
                             line = lines[i].strip()
                             if self._isConditionalMacroStart(line):
                                 deep = deep + 1
                             elif self._isConditionalMacroEnd(line):
                                 deep = deep - 1
                 elif self._isIfCondition(line):
                     if not self._isConditionTrue(line):
                         # skip conditional body
                         deep = 1
                         while i < totalLines and deep != 0:
                             i = i + 1
                             line = lines[i].strip()
                             if self._isConditionalMacroStart(line):
                                 deep = deep + 1
                             elif self._isConditionalMacroEnd(line):
                                 deep = deep - 1
                 elif self._isSpecMacro(line):
                     macro, i = self._readMacroFromFile(i, lines)
                     self._updateSpecMacro(macro)
                 elif self._isPackageMacro(line):
                     defaultpkg = self.packages.get('default')
                     returnVal, packageName = self._readPkgNameFromPackageMacro(line,
                                                                                defaultpkg.name)
                     packageName = self._replaceMacros(packageName)
                     if not returnVal:
                         return False
                     if line.startswith('%package'):
                         pkg = Package(defaultpkg)
                         pkg.name = packageName
                         currentPkg = packageName
                         self.packages[pkg.name] = pkg
                     else:
                         if defaultpkg.name == packageName:
                             packageName = 'default'
                         macro, i = self._readMacroFromFile(i, lines)
                         if packageName not in self.packages:
                             i = i + 1
                             continue
                         self.packages[packageName].updatePackageMacro(macro)
                 elif self._isPackageHeaders(line):
                     self._readPackageHeaders(line, self.packages[currentPkg])
                 elif self._isGlobalSecurityHardening(line):
                     self._readSecurityHardening(line)
                 elif self._isChecksum(line):
                     self._readChecksum(line, self.packages[currentPkg])
                 elif self._isDefinition(line):
                     self._readDefinition(line)
                 elif self._isConditionalCheckMacro(line):
                     self.conditionalCheckMacroEnabled = True
                 elif self.conditionalCheckMacroEnabled and self._isConditionalMacroEnd(line):
                     self.conditionalCheckMacroEnabled = False
                 else:
                     self.specAdditionalContent += line + "\n"
                 i = i + 1
5062126c
 
326d5ca8
     def _readPkgNameFromPackageMacro(self, data, basePkgName=None):
87815216
         data = " ".join(data.split())
         pkgHeaderName = data.split(" ")
2820c61a
         lenpkgHeaderName = len(pkgHeaderName)
87815216
         i = 1
adf248d5
         pkgName = None
87815216
         while i < lenpkgHeaderName:
adf248d5
             if pkgHeaderName[i] == "-n" and i+1 < lenpkgHeaderName:
                 pkgName = pkgHeaderName[i+1]
                 break
             if pkgHeaderName[i].startswith('-'):
                 i = i + 2
             else:
87815216
                 pkgName = basePkgName + "-" + pkgHeaderName[i]
adf248d5
                 break
         if pkgName is None:
2820c61a
             return True, basePkgName
adf248d5
         return True, pkgName
5062126c
 
326d5ca8
     def _replaceMacros(self, string):
343d89e8
         """Replace all macros in given string with corresponding values.
 
         For example: a string '%{name}-%{version}.tar.gz' will be transformed to 'foo-2.0.tar.gz'.
 
         :return A string where all macros in given input are substituted as good as possible.
 
         """
         def _is_conditional(macro):
87815216
             return macro.startswith(("?", "!"))
343d89e8
 
         def _test_conditional(macro):
             if macro[0] == "?":
                 return True
             if macro[0] == "!":
                 return False
             raise Exception("Given string is not a conditional macro")
 
         def _is_macro_defined(macro):
aac331d9
             return (macro in self.defs.keys()) or (macro in constants.userDefinedMacros.keys()) \
                 or (macro in constants.getAdditionalMacros(self.packages["default"].name).keys())
343d89e8
 
         def _get_macro(macro):
             if macro in self.defs.keys():
                 return self.defs[macro]
             elif macro in constants.userDefinedMacros.keys():
                 return constants.userDefinedMacros[macro]
aac331d9
             elif macro in constants.getAdditionalMacros(self.packages["default"].name).keys():
                 return constants.getAdditionalMacros(self.packages["default"].name)[macro]
343d89e8
             raise Exception("Unknown macro: " + macro)
 
         def _macro_repl(match):
             macro_name = match.group(1)
             if _is_conditional(macro_name):
                 parts = macro_name[1:].split(":")
326d5ca8
                 assert parts
                 retv = ""
343d89e8
                 if _test_conditional(macro_name):  # ?
                     if _is_macro_defined(parts[0]):
                         if len(parts) == 2:
326d5ca8
                             retv = parts[1]
343d89e8
                         else:
326d5ca8
                             retv = _get_macro(parts[0])
343d89e8
                 else:  # !
                     if not _is_macro_defined(parts[0]):
                         if len(parts) == 2:
326d5ca8
                             retv = parts[1]
                 return retv
343d89e8
 
             if _is_macro_defined(macro_name):
                 return _get_macro(macro_name)
             return match.string[match.start():match.end()]
 
         #User macros
326d5ca8
         for macroName, value in constants.userDefinedMacros.items():
87815216
             macro = "%" + macroName
343d89e8
             if string.find(macro) != -1:
87815216
                 string = string.replace(macro, value)
343d89e8
         #Spec definitions
326d5ca8
         for macroName, value in self.defs.items():
87815216
             macro = "%" + macroName
343d89e8
             if string.find(macro) != -1:
87815216
                 string = string.replace(macro, value)
343d89e8
         return re.sub(self.macro_pattern, _macro_repl, string)
 
326d5ca8
     def _createDefaultPackage(self):
         self.packages["default"] = Package()
5062126c
 
326d5ca8
     def _readMacroFromFile(self, currentPos, lines):
2820c61a
         macro = rpmMacro()
         line = lines[currentPos]
         macro.position = currentPos
87815216
         macro.endposition = currentPos
         endPos = len(lines)
2820c61a
         line = " ".join(line.split())
         flagindex = line.find(" ")
         if flagindex != -1:
87815216
             macro.macroFlag = line[flagindex+1:]
             macro.macroName = line[:flagindex]
2820c61a
         else:
87815216
             macro.macroName = line
2820c61a
 
326d5ca8
         if currentPos + 1 < len(lines) and self._isMacro(lines[currentPos+1]):
87815216
             return macro, currentPos
5062126c
 
87815216
         for j in range(currentPos + 1, endPos):
2820c61a
             content = lines[j]
326d5ca8
             if j+1 < endPos and self._isMacro(lines[j+1]):
87815216
                 return macro, j
2820c61a
             macro.content += content +'\n'
87815216
             macro.endposition = j
         return macro, endPos
5062126c
 
326d5ca8
     def _updateSpecMacro(self, macro):
2820c61a
         if macro.macroName == "%clean":
87815216
             self.cleanMacro = macro
2820c61a
             return True
         if macro.macroName == "%prep":
87815216
             self.prepMacro = macro
2820c61a
             return True
         if macro.macroName == "%build":
87815216
             self.buildMacro = macro
2820c61a
             return True
         if macro.macroName == "%install":
87815216
             self.installMacro = macro
2820c61a
             return True
         if macro.macroName == "%changelog":
87815216
             self.changelogMacro = macro
2820c61a
             return True
         if macro.macroName == "%check":
87815216
             self.checkMacro = macro
2820c61a
             return True
         return False
326d5ca8
     def _isMacro(self, line):
         return (self._isPackageMacro(line) or
                 self._isSpecMacro(line) or
                 self._isConditionalMacroStart(line) or
                 self._isConditionalMacroEnd(line))
5062126c
 
326d5ca8
     def _isConditionalArch(self, line):
87815216
         if re.search('^'+'%ifarch', line):
0f1fdc4b
             return True
         return False
5062126c
 
326d5ca8
     def _isSpecMacro(self, line):
87815216
         if line.startswith(('%clean', '%prep', '%build', '%install', '%changelog', '%check')):
2820c61a
             return True
         return False
5062126c
 
326d5ca8
     def _isPackageMacro(self, line):
87815216
         line = line.strip()
         if line.startswith(('%post', '%postun', '%files', '%description', '%package')):
2820c61a
             return True
         return False
5062126c
 
326d5ca8
     def _isPackageHeaders(self, line):
87815216
         headersPatterns = ['^summary:', '^name:', '^group:',
                            '^license:', '^version:', '^release:',
                            '^distribution:', '^requires:',
326d5ca8
                            r'^requires\((pre|post|preun|postun)\):',
87815216
                            '^provides:', '^obsoletes:', '^conflicts:',
                            '^url:', '^source[0-9]*:', '^patch[0-9]*:',
                            '^buildrequires:', '^buildprovides:',
                            '^buildarch:']
         if any([re.search(r, line, flags=re.IGNORECASE) for r in headersPatterns]):
2820c61a
             return True
         return False
 
326d5ca8
     def _isGlobalSecurityHardening(self, line):
87815216
         if re.search('^%global *security_hardening', line, flags=re.IGNORECASE):
cb4e8710
             return True
         return False
 
326d5ca8
     def _isChecksum(self, line):
87815216
         if re.search('^%define *sha1', line, flags=re.IGNORECASE):
3cc43c92
             return True
         return False
 
326d5ca8
     def _isDefinition(self, line):
87815216
         if line.startswith(('%define', '%global')):
fba234bc
             return True
         return False
 
326d5ca8
     def _readConditionalArch(self, line):
87815216
         w = line.split()
0f1fdc4b
         if len(w) == 2:
87815216
             return w[1]
0f1fdc4b
         return None
 
326d5ca8
     def _readDefinition(self, line):
87815216
         listDefines = line.split()
fba234bc
         if len(listDefines) == 3:
326d5ca8
             self.defs[listDefines[1]] = self._replaceMacros(listDefines[2])
87815216
             return True
fba234bc
         return False
 
326d5ca8
     def _readHeader(self, line):
87815216
         headerSplitIndex = line.find(":")
         if headerSplitIndex + 1 == len(line):
             print(line)
             print("Error:Invalid header")
             return False, None, None
         headerName = line[0:headerSplitIndex].lower()
         headerContent = line[headerSplitIndex + 1:].strip()
         return True, headerName, headerContent
 
326d5ca8
     def _readDependentPackageData(self, line):
2820c61a
         strUtils = StringUtils()
87815216
         listPackages = line.split(",")
         listdependentpkgs = []
2820c61a
         for line in listPackages:
87815216
             line = strUtils.getStringInConditionalBrackets(line)
             listContents = line.split()
2820c61a
             totalContents = len(listContents)
87815216
             i = 0
2820c61a
             while i < totalContents:
                 dpkg = dependentPackageData()
87815216
                 compare = None
                 packageName = listContents[i]
f5cac196
                 if listContents[i].startswith("/"):
87815216
                     provider = constants.providedBy.get(listContents[i], None)
                     i += 1
45c9260c
                     if provider is not None:
87815216
                         packageName = provider
45c9260c
                     else:
                         continue
326d5ca8
                 if i + 2 < len(listContents):
f93ef2b0
                     if listContents[i+1] in (">=", "<=", "=", "<", ">"):
                         compare = listContents[i+1]
5062126c
 
2820c61a
                 if compare is not None:
87815216
                     dpkg.package = packageName
                     dpkg.compare = compare
                     dpkg.version = listContents[i+2]
                     i = i + 3
2820c61a
                 else:
87815216
                     dpkg.package = packageName
                     i = i + 1
2820c61a
                 listdependentpkgs.append(dpkg)
         return listdependentpkgs
 
326d5ca8
     def _readPackageHeaders(self, line, pkg):
         returnVal, headerName, headerContent = self._readHeader(line)
2820c61a
         if not returnVal:
             return False
 
326d5ca8
         headerContent = self._replaceMacros(headerContent)
2820c61a
         if headerName == 'summary':
87815216
             pkg.summary = headerContent
2820c61a
             return True
         if headerName == 'name':
87815216
             pkg.name = headerContent
             if pkg == self.packages["default"]:
343d89e8
                 self.defs["name"] = pkg.name
2820c61a
             return True
         if headerName == 'group':
87815216
             pkg.group = headerContent
2820c61a
             return True
         if headerName == 'license':
87815216
             pkg.license = headerContent
2820c61a
             return True
         if headerName == 'version':
87815216
             pkg.version = headerContent
             if pkg == self.packages["default"]:
343d89e8
                 self.defs["version"] = pkg.version
2820c61a
             return True
         if headerName == 'buildarch':
87815216
             pkg.buildarch = headerContent
2820c61a
             return True
         if headerName == 'release':
87815216
             pkg.release = headerContent
             if pkg == self.packages["default"]:
343d89e8
                 self.defs["release"] = pkg.release
2820c61a
             return True
         if headerName == 'distribution':
87815216
             pkg.distribution = headerContent
2820c61a
             return True
920a9773
         if headerName == 'url':
87815216
             pkg.URL = headerContent
920a9773
             return True
326d5ca8
         if 'source' in headerName:
2820c61a
             pkg.sources.append(headerContent)
             return True
326d5ca8
         if 'patch' in headerName:
2820c61a
             pkg.patches.append(headerContent)
             return True
87815216
         if (headerName.startswith('requires') or
                 headerName == 'provides' or
                 headerName == 'obsoletes' or
                 headerName == 'conflicts' or
                 headerName == 'buildrequires' or
                 headerName == 'buildprovides'):
326d5ca8
             dpkg = self._readDependentPackageData(headerContent)
2820c61a
             if dpkg is None:
                 return False
45c9260c
             if headerName.startswith('requires'):
2820c61a
                 pkg.requires.extend(dpkg)
             if headerName == 'provides':
                 pkg.provides.extend(dpkg)
             if headerName == 'obsoletes':
                 pkg.obsoletes.extend(dpkg)
             if headerName == 'conflicts':
                 pkg.conflicts.extend(dpkg)
             if headerName == 'buildrequires':
5f40784b
                 if self.conditionalCheckMacroEnabled:
                     pkg.checkbuildrequires.extend(dpkg)
                 else:
                     pkg.buildrequires.extend(dpkg)
2820c61a
             if headerName == 'buildprovides':
                 pkg.buildprovides.extend(dpkg)
5062126c
 
2820c61a
             return True
         return False
cb4e8710
 
326d5ca8
     def _readSecurityHardening(self, line):
87815216
         data = line.lower().strip()
         words = data.split(" ")
cb4e8710
         nrWords = len(words)
87815216
         if nrWords != 3:
             print("Error: Unable to parse line: " + line)
cb4e8710
             return False
87815216
         if words[2] != "none" and words[2] != "nonow" and words[2] != "nopie":
             print("Error: Invalid security_hardening value: " + words[2])
cb4e8710
             return False
         self.globalSecurityHardening = words[2]
87815216
         return True
3cc43c92
 
326d5ca8
     def _readChecksum(self, line, pkg):
3cc43c92
         strUtils = StringUtils()
326d5ca8
         line = self._replaceMacros(line)
87815216
         data = line.strip()
         words = data.split()
3cc43c92
         nrWords = len(words)
87815216
         if nrWords != 3:
             print("Error: Unable to parse line: " + line)
3cc43c92
             return False
87815216
         value = words[2].split("=")
         if len(value) != 2:
             print("Error: Unable to parse line: "+line)
3cc43c92
             return False
87815216
         matchedSources = []
3cc43c92
         for source in pkg.sources:
87815216
             sourceName = strUtils.getFileNameFromURL(source)
326d5ca8
             if sourceName.startswith(value[0]):
3cc43c92
                 matchedSources.append(sourceName)
326d5ca8
         if not matchedSources:
87815216
             print("Error: Can not find match for sha1 " + value[0])
3cc43c92
             return False
326d5ca8
         if len(matchedSources) > 1:
87815216
             print("Error: Too many matched Sources:" +
                   ' '.join(matchedSources) + " for sha1 " + value[0])
3cc43c92
             return False
         pkg.checksums[sourceName] = value[1]
326d5ca8
         return True
5f40784b
 
326d5ca8
     def _isConditionalCheckMacro(self, line):
5f40784b
         data = line.strip()
         words = data.split()
         nrWords = len(words)
326d5ca8
         if nrWords != 2:
5f40784b
             return False
326d5ca8
         if words[0] != "%if" or words[1] != "%{with_check}":
5f40784b
             return False
         return True
 
326d5ca8
     def _isIfCondition(self, line):
343d89e8
         return line.startswith("%if ")
 
     # Supports only %if %{}
326d5ca8
     def _isConditionTrue(self, line):
343d89e8
         data = line.strip()
         words = data.split()
         nrWords = len(words)
         # condition like %if a > b is not supported
326d5ca8
         if nrWords != 2:
343d89e8
             return True
326d5ca8
         if self._replaceMacros(words[1]) == "0":
343d89e8
             return False
         return True
 
326d5ca8
     def _isConditionalMacroStart(self, line):
0f1fdc4b
         return line.startswith("%if")
 
326d5ca8
     def _isConditionalMacroEnd(self, line):
         return line.strip() == "%endif"