1# -*- coding: utf-8 -*-
2#
3# Copyright 2013 Oliver Tappe
4# Distributed under the terms of the MIT License.
5
6# -- Modules ------------------------------------------------------------------
7
8import codecs
9import copy
10import glob
11import logging
12import os
13import re
14import shutil
15import sys
16import tarfile
17import time
18import zipfile
19from subprocess import PIPE, Popen
20
21if sys.stdout.isatty():
22	colorWarning = '\033[1;36m'
23	colorError = '\033[1;35m'
24	colorReset = '\033[1;m'
25else:
26	colorWarning = ''
27	colorError = ''
28	colorReset = ''
29
30# -- MyTarInfo -------------------------------------------------------------
31
32class MyTarInfo(tarfile.TarInfo):
33	"""Override tarfile.TarInfo in order to automatically treat hardlinks
34	   contained in tar archives as symbolic links during extraction.
35	"""
36	@classmethod
37	def frombuf(cls, buf):
38		tarinfo = tarfile.TarInfo.frombuf(buf)
39		if tarinfo.type == tarfile.LNKTYPE:
40			tarinfo.type = tarfile.SYMTYPE
41			tarinfo.linkname = os.path.join(os.path.relpath(os.path.dirname(
42				tarinfo.linkname), os.path.dirname(tarinfo.name)),
43				os.path.basename(tarinfo.linkname))
44		return tarinfo
45
46	@classmethod
47	def fromtarfile(cls, theTarfile):
48		tarinfo = tarfile.TarInfo.fromtarfile(theTarfile)
49		if tarinfo.type == tarfile.LNKTYPE:
50			tarinfo.type = tarfile.SYMTYPE
51			tarinfo.linkname = os.path.join(os.path.relpath(os.path.dirname(
52				tarinfo.linkname), os.path.dirname(tarinfo.name)),
53				os.path.basename(tarinfo.linkname))
54		return tarinfo
55
56# path to haikuports-tree --------------------------------------------------
57haikuportsRepoUrl = 'https://github.com/haikuports/haikuports.git'
58
59# path to haikuporter-tree
60haikuporterRepoUrl = 'https://github.com/haikuports/haikuporter.git'
61
62def sysExit(message):
63	"""wrap invocation of sys.exit()"""
64
65	message = '\n'.join([colorError + 'Error: ' + line + colorReset
66		for line in message.split('\n')])
67	sys.exit(message)
68
69def warn(message):
70	"""print a warning"""
71
72	message = '\n'.join([colorWarning + 'Warning: ' + line + colorReset
73		for line in message.split('\n')])
74	logging.getLogger("buildLogger").warn(message)
75
76def info(message):
77	"""print an info"""
78	if message is not None and message != '':
79		logging.getLogger("buildLogger").info(message if message[-1] != '\n'
80			else message[:-1])
81
82def printError(*args):
83	"""print a to stderr"""
84
85	sys.stderr.write(' '.join([str(arg) for arg in args]) + '\n')
86
87
88def escapeForPackageInfo(string):
89	"""escapes string to be used within "" quotes in a .PackageInfo file"""
90
91	return string.replace('\\', '\\\\').replace('"', '\\"')
92
93def unpackArchive(archiveFile, targetBaseDir, subdir):
94	"""Unpack archive into a directory"""
95
96	## REFACTOR into separate functions and dispatch
97
98	process = None
99	if not tarfile.is_tarfile(archiveFile):
100		ext = archiveFile.split('/')[-1].split('.')[-1]
101		if ext == 'lz':
102			ensureCommandIsAvailable('lzip')
103			process = Popen(['lzip', '-c', '-d', archiveFile],
104				bufsize=10240, stdin=PIPE, stdout=PIPE, stderr=PIPE)
105		elif ext == '7z':
106			ensureCommandIsAvailable('7za')
107			process = Popen(['7za', 'x', '-so', archiveFile],
108				bufsize=10240, stdin=PIPE, stdout=PIPE, stderr=PIPE)
109		elif ext == 'zst':
110			ensureCommandIsAvailable('zstd')
111			process = Popen(['zstd', '-c', '-d', archiveFile],
112				bufsize=10240, stdin=PIPE, stdout=PIPE, stderr=PIPE)
113
114	if subdir and not subdir.endswith('/'):
115		subdir += '/'
116	# unpack source archive or the decompressed stream
117	if process or tarfile.is_tarfile(archiveFile):
118		tarFile = None
119		if process:
120			tarFile = tarfile.open(fileobj=process.stdout, mode='r|',
121				tarinfo=MyTarInfo)
122		else:
123			tarFile = tarfile.open(archiveFile, 'r', tarinfo=MyTarInfo)
124
125		if subdir is None:
126			tarFile.extractall(path=targetBaseDir)
127		else:
128			def filterByDir(members):
129				for member in members:
130					member = copy.copy(member)
131					if (os.path.normpath(member.name).startswith(subdir)
132							and not os.path.normpath(member.name).endswith("/.git")):
133						if hasattr(os, "geteuid") and os.geteuid() == 0:
134							member.gname = ""
135							member.uname = ""
136							member.gid = 0
137							member.uid = 0
138						yield member
139			tarFile.extractall(members=filterByDir(tarFile), path=targetBaseDir)
140
141		tarFile.close()
142	elif zipfile.is_zipfile(archiveFile):
143		zipFile = zipfile.ZipFile(archiveFile, 'r')
144		names = None
145		if subdir:
146			names = [
147				name for name in zipFile.namelist()
148				if os.path.normpath(name).startswith(subdir)
149			]
150			if not names:
151				sysExit('sub-directory %s not found in archive' % subdir)
152		zipFile.extractall(targetBaseDir, names)
153		zipFile.close()
154	else:
155		sysExit('Unrecognized archive type in file '
156				+ archiveFile)
157
158def symlinkDirectoryContents(sourceDir, targetDir, emptyTargetDirFirst=True):
159	"""Populates targetDir with symlinks to all files from sourceDir"""
160
161	files = [sourceDir + '/' + fileName for fileName in os.listdir(sourceDir)]
162	symlinkFiles(files, targetDir)
163
164def symlinkGlob(globSpec, targetDir, emptyTargetDirFirst=True):
165	"""Populates targetDir with symlinks to all files matching given globSpec"""
166
167	files = glob.glob(globSpec)
168	symlinkFiles(files, targetDir)
169
170def symlinkFiles(sourceFiles, targetDir, emptyTargetDirFirst=True):
171	"""Populates targetDir with symlinks to all the given files"""
172
173	if os.path.exists(targetDir) and emptyTargetDirFirst:
174		shutil.rmtree(targetDir)
175	if not os.path.exists(targetDir):
176		os.makedirs(targetDir)
177	for sourceFile in sourceFiles:
178		os.symlink(sourceFile, targetDir + '/' + os.path.basename(sourceFile))
179
180def touchFile(theFile, stamp=None):  # @DontTrace
181	"""Touches given file, making sure that its modification date is bumped"""
182
183	if stamp is not None:
184		t = time.mktime(stamp.timetuple())
185	if os.path.exists(theFile):
186		os.utime(theFile, None if stamp is None else (t, t))
187	else:
188		open(theFile, 'w').close()
189		if stamp is not None:
190			os.utime(theFile, (t, t))
191
192def storeStringInFile(string, theFile):
193	"""Stores the given string in the file with the given name"""
194
195	with codecs.open(theFile, 'w', 'utf-8') as fo:
196		fo.write(string)
197
198def readStringFromFile(theFile):
199	"""Returns the contents of the file with the given name as a string"""
200
201	with codecs.open(theFile, 'r', 'utf-8') as fo:
202		return fo.read()
203
204availableCommands = {}
205def isCommandAvailable(command):
206	"""returns whether the given command is available"""
207
208	if command in availableCommands:
209		return availableCommands[command]
210
211	for path in os.environ['PATH'].split(':'):
212		if os.path.exists(path + '/' + command):
213			availableCommands[command] = True
214			return True
215
216	availableCommands[command] = False
217	return False
218
219def ensureCommandIsAvailable(command):
220	"""checks if the given command is available and bails if not"""
221
222	if not isCommandAvailable(command):
223		sysExit("'" + command + u"' is not available, please install it")
224
225def cmp(a, b):
226	return (a > b) - (a < b)
227
228def naturalCompare(left, right):
229	"""performs a natural compare between the two given strings - returns:
230		-1 if left is lower than right
231		 1 if left is higher than right
232		 0 if both are equal"""
233
234	convert = lambda text: int(text) if text.isdigit() else text.lower()
235	alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
236	return cmp(alphanum_key(left), alphanum_key(right))
237
238def bareVersionCompare(left, right):
239	"""Compares two given bare versions - returns:
240		-1 if left is lower than right
241		 1 if left is higher than right
242		 0 if both versions are equal"""
243
244	leftElements = left.split('.')
245	rightElements = right.split('.')
246
247	index = 0
248	leftElementCount = len(leftElements)
249	rightElementCount = len(rightElements)
250	while True:
251		if index + 1 > leftElementCount:
252			if index + 1 > rightElementCount:
253				return 0
254			else:
255				return -1
256		elif index + 1 > rightElementCount:
257			return 1
258
259		result = naturalCompare(leftElements[index], rightElements[index])
260		if result != 0:
261			return result
262
263		index += 1
264
265def versionCompare(left, right):
266	"""Compares two given versions that may include a pre-release - returns
267		-1 if left is lower than right
268		 1 if left is higher than right
269		 0 if both versions are equal"""
270
271	leftElements = left.split('~', 1)
272	rightElements = right.split('~', 1)
273
274	result = bareVersionCompare(leftElements[0], rightElements[0])
275	if result != 0:
276		return result
277
278	if len(leftElements) < 2:
279		if len(rightElements) < 2:
280			return 0
281		else:
282			return 1
283	elif len(rightElements) < 2:
284		return -1
285
286	# compare pre-release strings
287	return naturalCompare(leftElements[1], rightElements[1])
288
289def filteredEnvironment():
290	"""returns a filtered version of os.environ, such that none of the
291	   variables that we export for one port leak into the shell environment
292	   of another"""
293
294	env = {}
295
296	for key in ['LANG', 'LIBRARY_PATH', 'PATH']:
297		if key in os.environ:
298			env[key] = os.environ[key]
299
300	return env
301
302def prefixLines(prefix, string):
303	"""prefixes each line in the given string by prefix"""
304	return '\n'.join('{}{}'.format(prefix, line) for line in string.split('\n'))
305