1#!/usr/local/bin/python3.8 2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 3 4 5__license__ = 'GPL v3' 6__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' 7__docformat__ = 'restructuredtext en' 8 9import errno 10import os 11import platform 12import re 13import shutil 14import subprocess 15import sys 16import tempfile 17import time 18import hashlib 19from contextlib import contextmanager 20from functools import lru_cache 21 22is64bit = platform.architecture()[0] == '64bit' 23iswindows = re.search('win(32|64)', sys.platform) 24ismacos = 'darwin' in sys.platform 25isfreebsd = 'freebsd' in sys.platform 26isnetbsd = 'netbsd' in sys.platform 27isdragonflybsd = 'dragonfly' in sys.platform 28isbsd = isnetbsd or isfreebsd or isdragonflybsd 29ishaiku = 'haiku1' in sys.platform 30islinux = not ismacos and not iswindows and not isbsd and not ishaiku 31sys.setup_dir = os.path.dirname(os.path.abspath(__file__)) 32SRC = os.path.abspath(os.path.join(os.path.dirname(sys.setup_dir), 'src')) 33sys.path.insert(0, SRC) 34sys.resources_location = os.path.join(os.path.dirname(SRC), 'resources') 35sys.extensions_location = os.path.abspath(os.environ.get('CALIBRE_SETUP_EXTENSIONS_PATH', os.path.join(SRC, 'calibre', 'plugins'))) 36sys.running_from_setup = True 37 38__version__ = __appname__ = modules = functions = basenames = scripts = None 39 40_cache_dir_built = False 41 42 43def newer(targets, sources): 44 if hasattr(targets, 'rjust'): 45 targets = [targets] 46 if hasattr(sources, 'rjust'): 47 sources = [sources] 48 for f in targets: 49 if not os.path.exists(f): 50 return True 51 ttimes = map(lambda x: os.stat(x).st_mtime, targets) 52 stimes = map(lambda x: os.stat(x).st_mtime, sources) 53 newest_source, oldest_target = max(stimes), min(ttimes) 54 return newest_source > oldest_target 55 56 57def dump_json(obj, path, indent=4): 58 import json 59 with open(path, 'wb') as f: 60 data = json.dumps(obj, indent=indent) 61 if not isinstance(data, bytes): 62 data = data.encode('utf-8') 63 f.write(data) 64 65 66@lru_cache 67def curl_supports_etags(): 68 return '--etag-compare' in subprocess.check_output(['curl', '--help', 'all']).decode('utf-8') 69 70 71def download_securely(url): 72 # We use curl here as on some OSes (OS X) when bootstrapping calibre, 73 # python will be unable to validate certificates until after cacerts is 74 # installed 75 if not curl_supports_etags(): 76 return subprocess.check_output(['curl', '-fsSL', url]) 77 url_hash = hashlib.sha1(url.encode('utf-8')).hexdigest() 78 cache_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.cache', 'download', url_hash) 79 os.makedirs(cache_dir, exist_ok=True) 80 subprocess.check_call( 81 ['curl', '-fsSL', '--etag-compare', 'etag.txt', '--etag-save', 'etag.txt', '-o', 'data.bin', url], 82 cwd=cache_dir 83 ) 84 with open(os.path.join(cache_dir, 'data.bin'), 'rb') as f: 85 return f.read() 86 87 88def build_cache_dir(): 89 global _cache_dir_built 90 ans = os.path.join(os.path.dirname(SRC), '.build-cache') 91 if not _cache_dir_built: 92 _cache_dir_built = True 93 try: 94 os.mkdir(ans) 95 except EnvironmentError as err: 96 if err.errno != errno.EEXIST: 97 raise 98 return ans 99 100 101def require_git_master(branch='master'): 102 if subprocess.check_output(['git', 'symbolic-ref', '--short', 'HEAD']).decode('utf-8').strip() != branch: 103 raise SystemExit('You must be in the {} git branch'.format(branch)) 104 105 106def require_clean_git(): 107 c = subprocess.check_call 108 p = subprocess.Popen 109 c('git rev-parse --verify HEAD'.split(), stdout=subprocess.DEVNULL) 110 c('git update-index -q --ignore-submodules --refresh'.split()) 111 if p('git diff-files --quiet --ignore-submodules'.split()).wait() != 0: 112 raise SystemExit('You have unstaged changes in your working tree') 113 if p('git diff-index --cached --quiet --ignore-submodules HEAD --'.split()).wait() != 0: 114 raise SystemExit('Your git index contains uncommitted changes') 115 116 117def initialize_constants(): 118 global __version__, __appname__, modules, functions, basenames, scripts 119 120 with open(os.path.join(SRC, 'calibre/constants.py'), 'rb') as f: 121 src = f.read().decode('utf-8') 122 nv = re.search(r'numeric_version\s+=\s+\((\d+), (\d+), (\d+)\)', src) 123 __version__ = '%s.%s.%s'%(nv.group(1), nv.group(2), nv.group(3)) 124 __appname__ = re.search(r'__appname__\s+=\s+(u{0,1})[\'"]([^\'"]+)[\'"]', 125 src).group(2) 126 with open(os.path.join(SRC, 'calibre/linux.py'), 'rb') as sf: 127 epsrc = re.compile(r'entry_points = (\{.*?\})', re.DOTALL).\ 128 search(sf.read().decode('utf-8')).group(1) 129 entry_points = eval(epsrc, {'__appname__': __appname__}) 130 131 def e2b(ep): 132 return re.search(r'\s*(.*?)\s*=', ep).group(1).strip() 133 134 def e2s(ep, base='src'): 135 return (base+os.path.sep+re.search(r'.*=\s*(.*?):', ep).group(1).replace('.', '/')+'.py').strip() 136 137 def e2m(ep): 138 return re.search(r'.*=\s*(.*?)\s*:', ep).group(1).strip() 139 140 def e2f(ep): 141 return ep[ep.rindex(':')+1:].strip() 142 143 basenames, functions, modules, scripts = {}, {}, {}, {} 144 for x in ('console', 'gui'): 145 y = x + '_scripts' 146 basenames[x] = list(map(e2b, entry_points[y])) 147 functions[x] = list(map(e2f, entry_points[y])) 148 modules[x] = list(map(e2m, entry_points[y])) 149 scripts[x] = list(map(e2s, entry_points[y])) 150 151 152initialize_constants() 153preferred_encoding = 'utf-8' 154prints = print 155warnings = [] 156 157 158def get_warnings(): 159 return list(warnings) 160 161 162def edit_file(path): 163 return subprocess.Popen([ 164 'vim', '-c', 'ALELint', '-c', 'ALEFirst', '-S', os.path.join(SRC, '../session.vim'), '-f', path 165 ]).wait() == 0 166 167 168class Command: 169 170 SRC = SRC 171 RESOURCES = os.path.join(os.path.dirname(SRC), 'resources') 172 description = '' 173 174 sub_commands = [] 175 176 def __init__(self): 177 self.d = os.path.dirname 178 self.j = os.path.join 179 self.a = os.path.abspath 180 self.b = os.path.basename 181 self.s = os.path.splitext 182 self.e = os.path.exists 183 self.orig_euid = os.geteuid() if hasattr(os, 'geteuid') else None 184 self.real_uid = os.environ.get('SUDO_UID', None) 185 self.real_gid = os.environ.get('SUDO_GID', None) 186 self.real_user = os.environ.get('SUDO_USER', None) 187 188 def drop_privileges(self): 189 if not islinux or ismacos or isfreebsd: 190 return 191 if self.real_user is not None: 192 self.info('Dropping privileges to those of', self.real_user+':', 193 self.real_uid) 194 if self.real_gid is not None: 195 os.setegid(int(self.real_gid)) 196 if self.real_uid is not None: 197 os.seteuid(int(self.real_uid)) 198 199 def regain_privileges(self): 200 if not islinux or ismacos or isfreebsd: 201 return 202 if os.geteuid() != 0 and self.orig_euid == 0: 203 self.info('Trying to get root privileges') 204 os.seteuid(0) 205 if os.getegid() != 0: 206 os.setegid(0) 207 208 def pre_sub_commands(self, opts): 209 pass 210 211 def running(self, cmd): 212 from setup.commands import command_names 213 if os.environ.get('CI'): 214 self.info('::group::' + command_names[cmd]) 215 self.info('\n*') 216 self.info('* Running', command_names[cmd]) 217 self.info('*\n') 218 219 def run_cmd(self, cmd, opts): 220 from setup.commands import command_names 221 cmd.pre_sub_commands(opts) 222 for scmd in cmd.sub_commands: 223 self.run_cmd(scmd, opts) 224 225 st = time.time() 226 self.running(cmd) 227 cmd.run(opts) 228 self.info('* %s took %.1f seconds' % (command_names[cmd], time.time() - st)) 229 if os.environ.get('CI'): 230 self.info('::endgroup::') 231 232 def run_all(self, opts): 233 self.run_cmd(self, opts) 234 235 def add_command_options(self, command, parser): 236 import setup.commands as commands 237 command.sub_commands = [getattr(commands, cmd) for cmd in 238 command.sub_commands] 239 for cmd in command.sub_commands: 240 self.add_command_options(cmd, parser) 241 242 command.add_options(parser) 243 244 def add_all_options(self, parser): 245 self.add_command_options(self, parser) 246 247 def run(self, opts): 248 pass 249 250 def add_options(self, parser): 251 pass 252 253 def clean(self): 254 pass 255 256 @classmethod 257 def newer(cls, targets, sources): 258 ''' 259 Return True if sources is newer that targets or if targets 260 does not exist. 261 ''' 262 return newer(targets, sources) 263 264 def info(self, *args, **kwargs): 265 prints(*args, **kwargs) 266 sys.stdout.flush() 267 268 def warn(self, *args, **kwargs): 269 print('\n'+'_'*20, 'WARNING','_'*20) 270 prints(*args, **kwargs) 271 print('_'*50) 272 warnings.append((args, kwargs)) 273 sys.stdout.flush() 274 275 @contextmanager 276 def temp_dir(self, **kw): 277 ans = tempfile.mkdtemp(**kw) 278 try: 279 yield ans 280 finally: 281 shutil.rmtree(ans) 282 283 284def installer_name(ext, is64bit=False): 285 if is64bit and ext == 'msi': 286 return 'dist/%s-64bit-%s.msi'%(__appname__, __version__) 287 if ext in ('exe', 'msi'): 288 return 'dist/%s-%s.%s'%(__appname__, __version__, ext) 289 if ext == 'dmg': 290 if is64bit: 291 return 'dist/%s-%s-x86_64.%s'%(__appname__, __version__, ext) 292 return 'dist/%s-%s.%s'%(__appname__, __version__, ext) 293 294 ans = 'dist/%s-%s-i686.%s'%(__appname__, __version__, ext) 295 if is64bit: 296 ans = ans.replace('i686', 'x86_64') 297 return ans 298