1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5from __future__ import absolute_import, print_function, unicode_literals 6 7r"""This module contains code for managing clobbering of the tree.""" 8 9import errno 10import os 11import subprocess 12import sys 13 14from mozfile.mozfile import remove as mozfileremove 15from textwrap import TextWrapper 16 17 18CLOBBER_MESSAGE = "".join( 19 [ 20 TextWrapper().fill(line) + "\n" 21 for line in """ 22The CLOBBER file has been updated, indicating that an incremental build since \ 23your last build will probably not work. A full/clobber build is required. 24 25The reason for the clobber is: 26 27{clobber_reason} 28 29Clobbering can be performed automatically. However, we didn't automatically \ 30clobber this time because: 31 32{no_reason} 33 34The easiest and fastest way to clobber is to run: 35 36 $ mach clobber 37 38If you know this clobber doesn't apply to you or you're feeling lucky -- \ 39Well, are ya? -- you can ignore this clobber requirement by running: 40 41 $ touch {clobber_file} 42""".splitlines() 43 ] 44) 45 46 47class Clobberer(object): 48 def __init__(self, topsrcdir, topobjdir, substs=None): 49 """Create a new object to manage clobbering the tree. 50 51 It is bound to a top source directory and to a specific object 52 directory. 53 """ 54 assert os.path.isabs(topsrcdir) 55 assert os.path.isabs(topobjdir) 56 57 self.topsrcdir = os.path.normpath(topsrcdir) 58 self.topobjdir = os.path.normpath(topobjdir) 59 self.src_clobber = os.path.join(topsrcdir, "CLOBBER") 60 self.obj_clobber = os.path.join(topobjdir, "CLOBBER") 61 if substs: 62 self.substs = substs 63 else: 64 self.substs = dict() 65 66 # Try looking for mozilla/CLOBBER, for comm-central 67 if not os.path.isfile(self.src_clobber): 68 comm_clobber = os.path.join(topsrcdir, "mozilla", "CLOBBER") 69 if os.path.isfile(comm_clobber): 70 self.src_clobber = comm_clobber 71 72 def clobber_needed(self): 73 """Returns a bool indicating whether a tree clobber is required.""" 74 75 # No object directory clobber file means we're good. 76 if not os.path.exists(self.obj_clobber): 77 return False 78 79 # No source directory clobber means we're running from a source package 80 # that doesn't use clobbering. 81 if not os.path.exists(self.src_clobber): 82 return False 83 84 # Object directory clobber older than current is fine. 85 if os.path.getmtime(self.src_clobber) <= os.path.getmtime(self.obj_clobber): 86 87 return False 88 89 return True 90 91 def clobber_cause(self): 92 """Obtain the cause why a clobber is required. 93 94 This reads the cause from the CLOBBER file. 95 96 This returns a list of lines describing why the clobber was required. 97 Each line is stripped of leading and trailing whitespace. 98 """ 99 with open(self.src_clobber, "rt") as fh: 100 lines = [l.strip() for l in fh.readlines()] 101 return [l for l in lines if l and not l.startswith("#")] 102 103 def have_winrm(self): 104 # `winrm -h` should print 'winrm version ...' and exit 1 105 try: 106 p = subprocess.Popen( 107 ["winrm.exe", "-h"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT 108 ) 109 return p.wait() == 1 and p.stdout.read().startswith("winrm") 110 except Exception: 111 return False 112 113 def collect_subdirs(self, root, exclude): 114 """Gathers a list of subdirectories excluding specified items.""" 115 paths = [] 116 try: 117 for p in os.listdir(root): 118 if p not in exclude: 119 paths.append(os.path.join(root, p)) 120 except OSError as e: 121 if e.errno != errno.ENOENT: 122 raise 123 124 return paths 125 126 def delete_dirs(self, root, paths_to_delete): 127 """Deletes the given subdirectories in an optimal way.""" 128 procs = [] 129 for p in sorted(paths_to_delete): 130 path = os.path.join(root, p) 131 if ( 132 sys.platform.startswith("win") 133 and self.have_winrm() 134 and os.path.isdir(path) 135 ): 136 procs.append(subprocess.Popen(["winrm", "-rf", path])) 137 else: 138 # We use mozfile because it is faster than shutil.rmtree(). 139 mozfileremove(path) 140 141 for p in procs: 142 p.wait() 143 144 def remove_objdir(self, full=True): 145 """Remove the object directory. 146 147 ``full`` controls whether to fully delete the objdir. If False, 148 some directories (e.g. Visual Studio Project Files) will not be 149 deleted. 150 """ 151 # Determine where cargo build artifacts are stored 152 RUST_TARGET_VARS = ("RUST_HOST_TARGET", "RUST_TARGET") 153 rust_targets = set( 154 [self.substs[x] for x in RUST_TARGET_VARS if x in self.substs] 155 ) 156 rust_build_kind = "release" 157 if self.substs.get("MOZ_DEBUG_RUST"): 158 rust_build_kind = "debug" 159 160 # Top-level files and directories to not clobber by default. 161 no_clobber = {".mozbuild", "msvc", "_virtualenvs"} 162 163 # Hold off on clobbering cargo build artifacts 164 no_clobber |= rust_targets 165 166 if full: 167 paths = [self.topobjdir] 168 else: 169 paths = self.collect_subdirs(self.topobjdir, no_clobber) 170 171 self.delete_dirs(self.topobjdir, paths) 172 173 # Now handle cargo's build artifacts and skip removing the incremental 174 # compilation cache. 175 for target in rust_targets: 176 cargo_path = os.path.join(self.topobjdir, target, rust_build_kind) 177 paths = self.collect_subdirs( 178 cargo_path, 179 { 180 "incremental", 181 }, 182 ) 183 self.delete_dirs(cargo_path, paths) 184 185 def maybe_do_clobber(self, cwd, allow_auto=False, fh=sys.stderr): 186 """Perform a clobber if it is required. Maybe. 187 188 This is the API the build system invokes to determine if a clobber 189 is needed and to automatically perform that clobber if we can. 190 191 This returns a tuple of (bool, bool, str). The elements are: 192 193 - Whether a clobber was/is required. 194 - Whether a clobber was performed. 195 - The reason why the clobber failed or could not be performed. This 196 will be None if no clobber is required or if we clobbered without 197 error. 198 """ 199 assert cwd 200 cwd = os.path.normpath(cwd) 201 202 if not self.clobber_needed(): 203 print("Clobber not needed.", file=fh) 204 return False, False, None 205 206 # So a clobber is needed. We only perform a clobber if we are 207 # allowed to perform an automatic clobber (off by default) and if the 208 # current directory is not under the object directory. The latter is 209 # because operating systems, filesystems, and shell can throw fits 210 # if the current working directory is deleted from under you. While it 211 # can work in some scenarios, we take the conservative approach and 212 # never try. 213 if not allow_auto: 214 return ( 215 True, 216 False, 217 self._message( 218 "Automatic clobbering is not enabled\n" 219 ' (add "mk_add_options AUTOCLOBBER=1" to your ' 220 "mozconfig)." 221 ), 222 ) 223 224 if cwd.startswith(self.topobjdir) and cwd != self.topobjdir: 225 return ( 226 True, 227 False, 228 self._message( 229 "Cannot clobber while the shell is inside the object directory." 230 ), 231 ) 232 233 print("Automatically clobbering %s" % self.topobjdir, file=fh) 234 try: 235 self.remove_objdir(False) 236 print("Successfully completed auto clobber.", file=fh) 237 return True, True, None 238 except (IOError) as error: 239 return ( 240 True, 241 False, 242 self._message("Error when automatically clobbering: " + str(error)), 243 ) 244 245 def _message(self, reason): 246 lines = [" " + line for line in self.clobber_cause()] 247 248 return CLOBBER_MESSAGE.format( 249 clobber_reason="\n".join(lines), 250 no_reason=" " + reason, 251 clobber_file=self.obj_clobber, 252 ) 253