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