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