Source code

Revision control

Copy as Markdown

Other Tools

# 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/.
from __future__ import absolute_import, print_function, unicode_literals
r"""This module contains code for managing clobbering of the tree."""
import errno
import os
import subprocess
import sys
from mozfile.mozfile import remove as mozfileremove
from textwrap import TextWrapper
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,
)