1# -*- coding: utf-8 -*- 2# 3# Copyright 2017 Michael Lotz 4# Distributed under the terms of the MIT License. 5 6# -- Modules ------------------------------------------------------------------ 7 8import glob 9import hashlib 10import os 11import subprocess 12 13from .Configuration import Configuration 14from .DependencyResolver import DependencyResolver 15from .Options import getOption 16from .PackageInfo import PackageInfo 17from .Utils import info, prefixLines, sysExit, versionCompare, warn 18 19# -- PackageRepository class -------------------------------------------------- 20 21class PackageRepository(object): 22 23 def __init__(self, packagesPath, repository, quiet, verbose): 24 self.packagesPath = packagesPath 25 if not os.path.exists(self.packagesPath): 26 os.mkdir(self.packagesPath) 27 28 self.obsoleteDir = os.path.join(self.packagesPath, '.obsolete') 29 if not os.path.exists(self.obsoleteDir): 30 os.mkdir(self.obsoleteDir) 31 32 self.architectures = [Configuration.getTargetArchitecture(), 33 'any', 'source'] 34 35 self.repository = repository 36 self.quiet = quiet 37 self.verbose = verbose 38 39 def prune(self): 40 self.obsoletePackagesWithoutPort() 41 self.obsoletePackagesNewerThanActiveVersion() 42 self.obsoletePackagesWithNewerVersions() 43 44 def packageList(self, packageSpec=None): 45 if packageSpec is None: 46 packageSpec = '' 47 else: 48 packageSpec += '-' 49 50 packageSpec += '*.hpkg' 51 return glob.glob(os.path.join(self.packagesPath, packageSpec)) 52 53 def packageInfoList(self, packageSpec=None): 54 result = [] 55 for package in self.packageList(packageSpec): 56 try: 57 packageInfo = PackageInfo(package) 58 except Exception as exception: 59 warn('failed to get info of {}: {}'.format(package, exception)) 60 continue 61 62 if packageInfo.architecture not in self.architectures: 63 continue 64 65 result.append(packageInfo) 66 67 return result 68 69 def obsoletePackage(self, path, reason=None): 70 packageFileName = os.path.basename(path) 71 if not self.quiet: 72 print('\tobsoleting package {}: {}'.format(packageFileName, reason)) 73 74 os.rename(path, os.path.join(self.obsoleteDir, packageFileName)) 75 76 def obsoletePackagesForSpec(self, packageSpec, reason=None): 77 """remove all packages for the given packageSpec""" 78 79 for package in self.packageList(packageSpec): 80 self.obsoletePackage(package, reason) 81 82 def obsoletePackagesWithoutPort(self): 83 """remove packages that have no corresponding port""" 84 85 for package in self.packageInfoList(): 86 portName = self.repository.getPortNameForPackageName(package.name) 87 activePort = self.repository.getActivePort(portName) 88 if not activePort: 89 self.obsoletePackage(package.path, 'no port for it exists') 90 91 def obsoletePackagesNewerThanActiveVersion(self): 92 """remove packages newer than what their active port version produces""" 93 94 for package in self.packageInfoList(): 95 portName = self.repository.getPortNameForPackageName(package.name) 96 activePort = self.repository.getActivePort(portName) 97 if not activePort: 98 continue 99 100 if versionCompare(package.version, activePort.fullVersion) > 0: 101 self.obsoletePackage(package.path, 102 'newer than active {}'.format(activePort.fullVersion)) 103 104 def obsoletePackagesWithNewerVersions(self): 105 """remove all packages where newer version packages are available""" 106 107 newestPackages = dict() 108 reason = 'newer version {} available' 109 for package in self.packageInfoList(): 110 if package.name in newestPackages: 111 newest = newestPackages[package.name] 112 if versionCompare(newest.version, package.version) > 0: 113 self.obsoletePackage(package.path, 114 reason.format(newest.version)) 115 continue 116 117 self.obsoletePackage(newest.path, 118 reason.format(package.version)) 119 120 newestPackages[package.name] = package 121 122 def createPackageRepository(self, outputPath): 123 packageRepoCommand = Configuration.getPackageRepoCommand() 124 if not packageRepoCommand: 125 sysExit('package repo command must be configured or specified') 126 127 repoFile = os.path.join(outputPath, 'repo') 128 repoInfoFile = repoFile + '.info' 129 if not os.path.exists(repoInfoFile): 130 sysExit('repository info file expected at {}'.format(repoInfoFile)) 131 132 repoPackagesPath = os.path.join(outputPath, 'packages') 133 if not os.path.exists(repoPackagesPath): 134 os.mkdir(repoPackagesPath) 135 else: 136 for package in glob.glob(os.path.join(repoPackagesPath, '*.hpkg')): 137 os.unlink(package) 138 139 packageList = self.packageInfoList() 140 for package in packageList: 141 os.link(package.path, 142 os.path.join(repoPackagesPath, os.path.basename(package.path))) 143 144 packageListFile = os.path.join(outputPath, 'package.list') 145 with open(packageListFile, 'w') as outputFile: 146 fileList = '\n'.join( 147 [os.path.basename(package.path) for package in packageList]) 148 outputFile.write(fileList) 149 150 if not os.path.exists(repoFile): 151 if not packageList: 152 sysExit('no repo file exists and no packages to create it') 153 154 output = subprocess.check_output([packageRepoCommand, 'create', 155 '-v', repoInfoFile, packageList[0].path], 156 stderr=subprocess.STDOUT).decode('utf-8') 157 info(output) 158 159 output = subprocess.check_output([packageRepoCommand, 'update', '-C', 160 repoPackagesPath, '-v', repoFile, repoFile, packageListFile], 161 stderr=subprocess.STDOUT).decode('utf-8') 162 info(output) 163 self._checksumPackageRepository(repoFile) 164 self._signPackageRepository(repoFile) 165 166 def _checksumPackageRepository(self, repoFile): 167 """Create a checksum of the package repository""" 168 checksum = hashlib.sha256() 169 with open(repoFile, 'rb') as inputFile: 170 while True: 171 data = inputFile.read(1 * 1024 * 1024) 172 if not data: 173 break 174 checksum.update(data) 175 with open(repoFile + '.sha256', 'w') as outputFile: 176 outputFile.write(checksum.hexdigest()) 177 178 def _signPackageRepository(self, repoFile): 179 """Sign the package repository if a private key was provided""" 180 privateKeyFile = getOption('packageRepositorySignPrivateKeyFile') 181 privateKeyPass = getOption('packageRepositorySignPrivateKeyPass') 182 if not privateKeyFile and not privateKeyPass: 183 info("Warning: unsigned package repository") 184 return 185 if not os.path.exists(privateKeyFile): 186 sysExit('specified package repo private key file missing!') 187 188 if not os.path.exists(repoFile): 189 sysExit('no repo file was found to sign!') 190 191 minisignCommand = Configuration.getMinisignCommand() 192 if not minisignCommand: 193 sysExit('minisign command missing to sign repository!') 194 195 # minisign -s /tmp/minisign.key -Sm ${ARTIFACT} 196 info("signing repository") 197 output = subprocess.check_output([minisignCommand, '-s', 198 privateKeyFile, "-Sm", repoFile], input=privateKeyPass.encode('utf-8'), 199 stderr=subprocess.STDOUT).decode('utf-8') 200 info(output) 201 202 def checkPackageRepositoryConsistency(self): 203 """Check consistency of package repository by dependency solving all 204 all packages.""" 205 206 repositories = [self.packagesPath] 207 systemPackagesDirectory = getOption('systemPackagesDirectory') 208 if systemPackagesDirectory: 209 repositories.append(systemPackagesDirectory) 210 211 resolver = DependencyResolver(None, ['REQUIRES'], repositories, 212 quiet=True) 213 214 for package in self.packageInfoList(): 215 if self.verbose: 216 print('checking package {}'.format(package.path)) 217 218 try: 219 resolver.determineRequiredPackagesFor([package.path]) 220 except LookupError as error: 221 print('{}:\n{}\n'.format(os.path.relpath(package.path, 222 self.packagesPath), prefixLines('\t', str(error)))) 223