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