1# -*- coding: utf-8 -*- 2# 3# Copyright 2013 Oliver Tappe 4# Distributed under the terms of the MIT License. 5 6# -- Modules ------------------------------------------------------------------ 7 8import functools 9from subprocess import CalledProcessError, check_output 10 11from .RecipeTypes import (Architectures, Extendable, LinesOfText, 12 MachineArchitecture, Phase, ProvidesList, 13 RequiresList, Status, YesNo) 14from .ShellScriptlets import configFileEvaluatorScript, getShellVariableSetters 15from .Utils import filteredEnvironment, sysExit, warn 16 17# -- haikuports.conf and *.recipe parser -------------------------------- 18 19class ConfigParser(object): 20 def __init__(self, filename, attributes, shellVariables): 21 22 ## REFACTOR environment setup and conf location into a single function 23 ## that then calls the ConfigParser and either passes in the file path 24 ## or the contents of the file 25 26 # set up the shell environment -- we want it to inherit some of our 27 # variables 28 shellEnv = filteredEnvironment() 29 shellEnv['recipePhases'] = ' '.join(Phase.getAllowedValues()) 30 31 # execute the config file via the shell .... 32 supportedKeysString = '|'.join(attributes.keys()) 33 shellVariables = shellVariables.copy() 34 shellVariables['supportedKeysPattern'] = supportedKeysString 35 shellVariables['fileToParse'] = filename 36 37 wrapperScript = (getShellVariableSetters(shellVariables) 38 + configFileEvaluatorScript) 39 try: 40 output = check_output(['bash', '-c', wrapperScript], env=shellEnv).decode('utf-8') 41 except (OSError, CalledProcessError): 42 sysExit("Can't evaluate config file: " + filename) 43 44 # ... and collect the resulting configurations (one per line) 45 46 self.entriesByExtension = {} 47 self.definedPhases = [] 48 49 lines = output.splitlines() 50 for line in lines: 51 ## REFACTOR into a testable method that can parse a single line 52 key, separator, valueString = line.partition('=') 53 if not separator: 54 sysExit('evaluating file %s produced illegal ' 55 'key-values line:\n %s\nexpected "<key>=<value>"\n' 56 'output of configuration script was: %s\n' 57 % (filename, line, output)) 58 59 # some keys may have a package-specific extension, check: 60 if key in attributes: 61 # unextended key 62 baseKey = key 63 extension = '' 64 index = '1' 65 else: 66 baseKey = '' 67 subKeys = key.split('_') 68 while subKeys: 69 subKey = subKeys.pop(0) 70 baseKey += ('_' if baseKey else '') + subKey 71 if baseKey in attributes: 72 if attributes[baseKey]['extendable'] != Extendable.NO: 73 extension = '_'.join(subKeys) 74 break 75 if attributes[baseKey]['indexable']: 76 index = None 77 if len(subKeys) == 0: 78 index = '1' 79 break 80 if len(subKeys) == 1 and subKeys[0].isdigit(): 81 index = subKeys[0] 82 break 83 warn('Ignoring key %s in file %s' % (key, filename)) 84 continue 85 else: 86 # might be a <PHASE>_DEFINED 87 isPhaseKey = False 88 if key.endswith('_DEFINED'): 89 phase = key[:-8] 90 if phase in Phase.getAllowedValues(): 91 isPhaseKey = True 92 self.definedPhases.append(phase) 93 94 if not isPhaseKey: 95 # skip unsupported key, just in case 96 warn('Key %s in file %s is unsupported, ignoring it' 97 % (key, filename)) 98 continue 99 100 # create empty dictionary for new extension 101 if extension not in self.entriesByExtension: 102 self.entriesByExtension[extension] = {} 103 104 entries = self.entriesByExtension[extension] 105 106 valueString = valueString.replace(r'\n', '\n') 107 # replace quoted newlines by real newlines 108 109 if attributes[baseKey]['indexable']: 110 if baseKey not in entries: 111 entries[baseKey] = {} 112 113 ## REFACTOR into one method per if/elif branch 114 attrType = attributes[baseKey]['type'] 115 if attrType == bytes: 116 if attributes[baseKey]['indexable']: 117 entries[baseKey][index] = valueString 118 else: 119 entries[key] = valueString 120 elif attrType == int: 121 try: 122 if attributes[baseKey]['indexable']: 123 entries[baseKey][index] = int(valueString) 124 else: 125 entries[key] = int(valueString) 126 except ValueError: 127 sysExit('evaluating file %s produced illegal value ' 128 '"%s" for key %s, expected an <integer> value' 129 % (filename, valueString, key)) 130 elif attrType in [list, ProvidesList, RequiresList]: 131 values = [v.strip() for v in valueString.splitlines()] 132 values = [v for v in values if len(v) > 0] 133 # explicitly protect against '-' in names of provides or 134 # requires declarations 135 if attrType in [ProvidesList, RequiresList]: 136 values = [v.lower() for v in values] 137 for value in values: 138 if '-' in value.split()[0]: 139 sysExit('evaluating file %s produced illegal value ' 140 '"%s" for key %s\n' 141 'dashes are not allowed in provides- or ' 142 'requires declarations' 143 % (filename, value, key)) 144 if attributes[baseKey]['indexable']: 145 entries[baseKey][index] = values 146 else: 147 entries[key] = values 148 elif attrType == LinesOfText: 149 # like a list, but only strip empty lines in front of and 150 # after the text 151 values = [v.strip() for v in valueString.splitlines()] 152 while values and len(values[0]) == 0: 153 values.pop(0) 154 while values and len(values[-1]) == 0: 155 values.pop() 156 entries[key] = values 157 elif attrType == Phase: 158 if valueString.upper() not in Phase.getAllowedValues(): 159 sysExit('evaluating file %s\nproduced illegal value "%s" ' 160 'for key %s\nexpected one of: %s' 161 % (filename, valueString, key, 162 ','.join(Phase.getAllowedValues()))) 163 entries[key] = valueString.upper() 164 elif attrType == MachineArchitecture: 165 entries[key] = {} 166 knownArchitectures = MachineArchitecture.getAll() 167 valueString = valueString.lower() 168 if valueString not in knownArchitectures: 169 architectures = ','.join(knownArchitectures) 170 sysExit('%s refers to unknown machine-architecture %s\n' 171 'known machine-architectures: %s' 172 % (filename, valueString, architectures)) 173 entries[key] = valueString 174 elif attrType == Architectures: 175 entries[key] = {} 176 for value in [v.lower() for v in valueString.split()]: 177 architecture = '' 178 if value.startswith('?'): 179 status = Status.UNTESTED 180 architecture = value[1:] 181 elif value.startswith('!'): 182 status = Status.BROKEN 183 architecture = value[1:] 184 else: 185 status = Status.STABLE 186 architecture = value 187 knownArchitectures = Architectures.getAll() 188 if architecture == 'all': 189 for machineArch in MachineArchitecture.getAll(): 190 entries[key][machineArch] = status 191 else: 192 if architecture not in knownArchitectures: 193 architectures = ','.join(knownArchitectures) 194 sysExit('%s refers to unknown architecture %s\n' 195 'known architectures: %s' 196 % (filename, architecture, architectures)) 197 entries[key][architecture] = status 198 if 'any' in entries[key] and len(entries[key]) > 1: 199 sysExit("%s specifies both 'any' and other architectures" 200 % (filename)) 201 if 'source' in entries[key] and len(entries[key]) > 1: 202 sysExit("%s specifies both 'source' and other architectures" 203 % (filename)) 204 elif attrType == YesNo: 205 valueString = valueString.lower() 206 if valueString not in YesNo.getAllowedValues(): 207 sysExit("Value for %s should be 'yes' or 'no' in %s" 208 % (key, filename)) 209 entries[key] = YesNo.toBool(valueString) 210 else: 211 sysExit('type of key %s in file %s is unsupported' 212 % (key, filename)) 213 # for entries in self.entriesByExtension.values(): 214 # for key in entries: 215 # print key + " = " + str(entries[key]) 216 217 def getEntriesForExtension(self, extension): 218 if extension in self.entriesByExtension: 219 return self.entriesByExtension[extension] 220 else: 221 return {} 222 223 @property 224 def extensions(self): 225 return self.entriesByExtension.keys() 226 227 228 ## REFACTOR - consider using simple functions for this 229 @staticmethod 230 def splitItem(string): 231 components = [] 232 if not string: 233 return components 234 235 component = '' 236 inQuote = False 237 for c in string: 238 if inQuote: 239 component += c 240 if c == '"': 241 inQuote = False 242 continue 243 244 if c.isspace(): 245 if component: 246 components.append(component) 247 component = '' 248 continue 249 250 component += c 251 if c == '"': 252 inQuote = True 253 254 if component: 255 components.append(component) 256 component = '' 257 258 return components 259 260 @staticmethod 261 def splitItemAndUnquote(string): 262 components = ConfigParser.splitItem(string) 263 unquotedComponents = [] 264 for component in components: 265 if component and component[0] == '"' and component[-1] == '"': 266 # use a regex if this called a lot 267 component = component[1:-1] 268 unquotedComponents.append(component) 269 return unquotedComponents 270 271 @staticmethod 272 def configurationStringFromDict(config): 273 configurationString = '' 274 for key in config.keys(): 275 configurationString += key + '="' 276 277 if isinstance(config[key], list): 278 configurationString += functools.reduce( 279 lambda result, item: result + ' ' + item, config[key], 280 '').strip() 281 elif type(config[key]) is bool: 282 configurationString += 'yes' if config[key] else 'no' 283 else: 284 configurationString += str(config[key]) 285 286 configurationString += '"\n' 287 288 return configurationString 289