# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. r"""This module contains code for managing clobbering of the tree.""" import errno import os import subprocess import sys from textwrap import TextWrapper from mozfile.mozfile import remove as mozfileremove CLOBBER_MESSAGE = "".join( [ TextWrapper().fill(line) + "\n" for line in """ The CLOBBER file has been updated, indicating that an incremental build since \ your last build will probably not work. A full/clobber build is required. The reason for the clobber is: {clobber_reason} Clobbering can be performed automatically. However, we didn't automatically \ clobber this time because: {no_reason} The easiest and fastest way to clobber is to run: $ mach clobber If you know this clobber doesn't apply to you or you're feeling lucky -- \ Well, are ya? -- you can ignore this clobber requirement by running: $ touch {clobber_file} """.splitlines() ] ) class Clobberer(object): def __init__(self, topsrcdir, topobjdir, substs=None): """Create a new object to manage clobbering the tree. It is bound to a top source directory and to a specific object directory. """ assert os.path.isabs(topsrcdir) assert os.path.isabs(topobjdir) self.topsrcdir = os.path.normpath(topsrcdir) self.topobjdir = os.path.normpath(topobjdir) self.src_clobber = os.path.join(topsrcdir, "CLOBBER") self.obj_clobber = os.path.join(topobjdir, "CLOBBER") if substs: self.substs = substs else: self.substs = dict() # Try looking for mozilla/CLOBBER, for comm-central if not os.path.isfile(self.src_clobber): comm_clobber = os.path.join(topsrcdir, "mozilla", "CLOBBER") if os.path.isfile(comm_clobber): self.src_clobber = comm_clobber def clobber_needed(self): """Returns a bool indicating whether a tree clobber is required.""" # No object directory clobber file means we're good. if not os.path.exists(self.obj_clobber): return False # No source directory clobber means we're running from a source package # that doesn't use clobbering. if not os.path.exists(self.src_clobber): return False # Object directory clobber older than current is fine. if os.path.getmtime(self.src_clobber) <= os.path.getmtime(self.obj_clobber): return False return True def clobber_cause(self): """Obtain the cause why a clobber is required. This reads the cause from the CLOBBER file. This returns a list of lines describing why the clobber was required. Each line is stripped of leading and trailing whitespace. """ with open(self.src_clobber, "rt") as fh: lines = [l.strip() for l in fh.readlines()] return [l for l in lines if l and not l.startswith("#")] def have_winrm(self): # `winrm -h` should print 'winrm version ...' and exit 1 try: p = subprocess.Popen( ["winrm.exe", "-h"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) return p.wait() == 1 and p.stdout.read().startswith("winrm") except Exception: return False def collect_subdirs(self, root, exclude): """Gathers a list of subdirectories excluding specified items.""" paths = [] try: for p in os.listdir(root): if p not in exclude: paths.append(os.path.join(root, p)) except OSError as e: if e.errno != errno.ENOENT: raise return paths def delete_dirs(self, root, paths_to_delete): """Deletes the given subdirectories in an optimal way.""" procs = [] for p in sorted(paths_to_delete): path = os.path.join(root, p) if ( sys.platform.startswith("win") and self.have_winrm() and os.path.isdir(path) ): procs.append(subprocess.Popen(["winrm", "-rf", path])) else: # We use mozfile because it is faster than shutil.rmtree(). mozfileremove(path) for p in procs: p.wait() def remove_objdir(self, full=True): """Remove the object directory. ``full`` controls whether to fully delete the objdir. If False, some directories (e.g. Visual Studio Project Files) will not be deleted. """ # Determine where cargo build artifacts are stored RUST_TARGET_VARS = ("RUST_HOST_TARGET", "RUST_TARGET") rust_targets = set( [self.substs[x] for x in RUST_TARGET_VARS if x in self.substs] ) rust_build_kind = "release" if self.substs.get("MOZ_DEBUG_RUST"): rust_build_kind = "debug" # Top-level files and directories to not clobber by default. no_clobber = {".mozbuild", "msvc", "_virtualenvs"} # Hold off on clobbering cargo build artifacts no_clobber |= rust_targets if full: paths = [self.topobjdir] else: paths = self.collect_subdirs(self.topobjdir, no_clobber) self.delete_dirs(self.topobjdir, paths) # Now handle cargo's build artifacts and skip removing the incremental # compilation cache. for target in rust_targets: cargo_path = os.path.join(self.topobjdir, target, rust_build_kind) paths = self.collect_subdirs( cargo_path, { "incremental", }, ) self.delete_dirs(cargo_path, paths) def maybe_do_clobber(self, cwd, allow_auto=False, fh=sys.stderr): """Perform a clobber if it is required. Maybe. This is the API the build system invokes to determine if a clobber is needed and to automatically perform that clobber if we can. This returns a tuple of (bool, bool, str). The elements are: - Whether a clobber was/is required. - Whether a clobber was performed. - The reason why the clobber failed or could not be performed. This will be None if no clobber is required or if we clobbered without error. """ assert cwd cwd = os.path.normpath(cwd) if not self.clobber_needed(): print("Clobber not needed.", file=fh) return False, False, None # So a clobber is needed. We only perform a clobber if we are # allowed to perform an automatic clobber (off by default) and if the # current directory is not under the object directory. The latter is # because operating systems, filesystems, and shell can throw fits # if the current working directory is deleted from under you. While it # can work in some scenarios, we take the conservative approach and # never try. if not allow_auto: return ( True, False, self._message( "Automatic clobbering is not enabled\n" ' (add "mk_add_options AUTOCLOBBER=1" to your ' "mozconfig)." ), ) if cwd.startswith(self.topobjdir) and cwd != self.topobjdir: return ( True, False, self._message( "Cannot clobber while the shell is inside the object directory." ), ) print("Automatically clobbering %s" % self.topobjdir, file=fh) try: self.remove_objdir(False) print("Successfully completed auto clobber.", file=fh) return True, True, None except (IOError) as error: return ( True, False, self._message("Error when automatically clobbering: " + str(error)), ) def _message(self, reason): lines = [" " + line for line in self.clobber_cause()] return CLOBBER_MESSAGE.format( clobber_reason="\n".join(lines), no_reason=" " + reason, clobber_file=self.obj_clobber, )