1# -*- coding: utf-8 -*- 2# 3# Copyright 2013-2014 Haiku, Inc. 4# Distributed under the terms of the MIT License. 5 6# -- Modules ------------------------------------------------------------------ 7 8import codecs 9import json 10import os 11import pickle 12import re 13from copy import deepcopy 14from subprocess import check_output 15 16from .Configuration import Configuration 17from .Utils import sysExit 18 19# -- Resolvable class --------------------------------------------------------- 20 21class Resolvable(object): 22 # HPKG: <name> [ <op> <version> [ "(compatible >= " <version> ")" ]] 23 # Recipe: <name> [ <op> <version> [ "compat >= " <version> ]] 24 versionPattern = re.compile(r'([^\s=]+)\s*(=\s*([^\s]+)\s*' 25 + r'((\(compatible|compat)\s*>=\s*([^\s)]+))?)?') 26 27 def __init__(self, string): 28 match = Resolvable.versionPattern.match(string) 29 self.name = match.group(1) 30 self.version = match.group(3) 31 self.compatibleVersion = match.group(6) 32 33 def __str__(self): 34 result = self.name 35 if self.version: 36 result += ' = ' + self.version 37 if self.compatibleVersion: 38 result += ' (compatible >= ' + self.compatibleVersion + ')' 39 return result 40 41 42# -- ResolvableExpression class ----------------------------------------------- 43 44class ResolvableExpression(object): 45 expressionPattern = re.compile(r'([^\s=!<>]+)\s*([=!<>]+)?\s*([^\s]+)?') 46 47 def __init__(self, string, ignoreBase=False): 48 match = ResolvableExpression.expressionPattern.match(string) 49 self.name = match.group(1) 50 self.operator = match.group(2) 51 self.version = match.group(3) 52 self.base = not ignoreBase and string.endswith(' base') 53 54 def __str__(self): 55 result = self.name 56 if self.operator: 57 result += ' ' + self.operator + ' ' + self.version 58 if self.base: 59 result += ' base' 60 return result 61 62 63# -- PackageInfo class -------------------------------------------------------- 64 65class PackageInfo(object): 66 hpkgCache = None 67 hpkgCacheDir = None 68 hpkgCachePath = None 69 70 def __init__(self, path): 71 self.path = path 72 73 if path.endswith('.hpkg') or path.endswith('.PackageInfo'): 74 self._parseFromHpkgOrPackageInfoFile() 75 elif path.endswith('.DependencyInfo'): 76 self._parseFromDependencyInfoFile() 77 else: 78 sysExit("don't know how to extract package-info from " + path) 79 80 @property 81 def versionedName(self): 82 return self.name + '-' + self.version 83 84 @classmethod 85 def _initializeCache(cls): 86 cls.hpkgCache = {} 87 cls.hpkgCacheDir = Configuration.getRepositoryPath() 88 cls.hpkgCachePath = os.path.join(cls.hpkgCacheDir, 'hpkgInfoCache') 89 if not os.path.exists(cls.hpkgCachePath): 90 return 91 92 prune = False 93 with open(cls.hpkgCachePath, 'rb') as cacheFile: 94 while True: 95 try: 96 entry = pickle.load(cacheFile) 97 path = entry['path'] 98 if not os.path.exists(path) \ 99 or os.path.getmtime(path) > entry['modifiedTime']: 100 prune = True 101 continue 102 103 cls.hpkgCache[path] = entry 104 except EOFError: 105 break 106 107 if prune: 108 with open(cls.hpkgCachePath, 'wb') as cacheFile: 109 for entry in cls.hpkgCache.values(): 110 pickle.dump(entry, cacheFile, pickle.HIGHEST_PROTOCOL) 111 112 @classmethod 113 def _writeToCache(cls, packageInfo): 114 cls.hpkgCache[packageInfo['path']] = deepcopy(packageInfo) 115 if not os.path.exists(cls.hpkgCacheDir): 116 os.makedirs(cls.hpkgCacheDir) 117 118 with open(cls.hpkgCachePath, 'ab') as cacheFile: 119 pickle.dump(packageInfo, cacheFile, pickle.HIGHEST_PROTOCOL) 120 121 def _parseFromHpkgOrPackageInfoFile(self, silent=False): 122 if self.path.endswith('.hpkg'): 123 if PackageInfo.hpkgCache is None: 124 PackageInfo._initializeCache() 125 126 if self.path in PackageInfo.hpkgCache: 127 self.__dict__ = deepcopy(PackageInfo.hpkgCache[self.path]) 128 return 129 130 # get an attribute listing of the package/package info file 131 args = [Configuration.getPackageCommand(), 'list', '-i', self.path] 132 if silent: 133 with open(os.devnull, "w") as devnull: 134 output = check_output(args, stderr=devnull).decode('utf-8') 135 else: 136 output = check_output(args).decode('utf-8') 137 138 # get various single-occurrence fields 139 self.name = self._extractField(output, 'name') 140 self.version = self._extractField(output, 'version') 141 self.architecture = self._extractField(output, 'architecture') 142 self.installPath = self._extractOptionalField(output, 'install path') 143 144 # get provides and requires (no buildrequires or -prerequires exist) 145 self.provides = [] 146 self.requires = [] 147 self.buildRequires = [] 148 self.buildPrerequires = [] 149 self.testRequires = [] 150 for line in output.splitlines(): 151 line = line.strip() 152 if line.startswith('provides:'): 153 self.provides.append(Resolvable(line[9:].lstrip())) 154 elif line.startswith('requires:'): 155 self.requires.append(ResolvableExpression(line[9:].lstrip(), 156 True)) 157 158 if self.path.endswith('.hpkg'): 159 self.modifiedTime = os.path.getmtime(self.path) 160 PackageInfo._writeToCache(self.__dict__) 161 162 def _parseFromDependencyInfoFile(self): 163 with codecs.open(self.path, 'r', 'utf-8') as fh: 164 dependencyInfo = json.load(fh) 165 166 # get various single-occurrence fields 167 self.name = dependencyInfo['name'] 168 self.version = dependencyInfo['version'] 169 self.architecture = dependencyInfo['architecture'] 170 171 # get provides and requires 172 self.provides = [ 173 Resolvable(p) for p in dependencyInfo['provides'] 174 ] 175 self.requires = [ 176 ResolvableExpression(r) for r in dependencyInfo['requires'] 177 ] 178 self.buildRequires = [ 179 ResolvableExpression(r) for r in dependencyInfo['buildRequires'] 180 ] 181 self.buildPrerequires = [ 182 ResolvableExpression(r) for r in dependencyInfo['buildPrerequires'] 183 ] 184 self.testRequires = [ 185 ResolvableExpression(r) for r in dependencyInfo['testRequires'] 186 ] 187 188 def _extractField(self, output, fieldName): 189 result = self._extractOptionalField(output, fieldName) 190 if not result: 191 sysExit('Failed to get %s of package "%s"' % (fieldName, self.path)) 192 return result 193 194 def _extractOptionalField(self, output, fieldName): 195 regExp = re.compile(r'^\s*%s:\s*(\S+)' % fieldName, re.MULTILINE) 196 match = regExp.search(output) 197 if match: 198 return match.group(1) 199 return None 200