Bug 1241398 - Add a dry-run mode to `mach build-backend`. r=gps
authorMike Hommey <mh+mozilla@glandium.org>
Thu, 21 Jan 2016 16:05:04 +0900
changeset 281003 012e4a8649219b7972a878ff9b9983ef80437370
parent 281002 61577ee979217944298cf2c62e42e9d6d9d7a1e4
child 281004 abd3c51d6eb1146dee3147845cfa65e954445eca
push id70653
push usermh@glandium.org
push dateThu, 21 Jan 2016 22:01:00 +0000
treeherdermozilla-inbound@38c6162cefb9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1241398
milestone46.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1241398 - Add a dry-run mode to `mach build-backend`. r=gps When doing build system changes affecting backend-generated files, I often use `mach build-backend --diff`. But most of the time I end up wanting to look at the full diff again when doing further changes, which leads me to stash my changes away, run `mach build-backend` to get the initial state again, unstash and rerun `mach build-backend --diff`. This has been a time drain for long enough :)
python/mozbuild/mozbuild/backend/base.py
python/mozbuild/mozbuild/config_status.py
python/mozbuild/mozbuild/mach_commands.py
python/mozbuild/mozbuild/util.py
--- a/python/mozbuild/mozbuild/backend/base.py
+++ b/python/mozbuild/mozbuild/backend/base.py
@@ -78,16 +78,18 @@ class BuildBackend(LoggingMixin):
 
         # The total wall time spent in the backend. This counts the time the
         # backend writes out files, etc.
         self._execution_time = 0.0
 
         # Mapping of changed file paths to diffs of the changes.
         self.file_diffs = {}
 
+        self.dry_run = False
+
         self._init()
 
     def summary(self):
         return ExecutionSummary(
             self.__class__.__name__.replace('Backend', '') +
             ' backend executed in {execution_time:.2f}s\n  '
             '{total:d} total backend files; '
             '{created:d} created; '
@@ -144,17 +146,18 @@ class BuildBackend(LoggingMixin):
                 with open(full_path, 'r') as existing:
                     old_content = existing.read()
                     if old_content:
                         self.file_diffs[full_path] = simple_diff(
                             full_path, old_content.splitlines(), None)
             except IOError:
                 pass
             try:
-                os.unlink(full_path)
+                if not self.dry_run:
+                    os.unlink(full_path)
                 self._deleted_count += 1
             except OSError:
                 pass
         # Remove now empty directories
         for dir in set(mozpath.dirname(d) for d in delete_files):
             try:
                 os.removedirs(dir)
             except OSError:
@@ -191,17 +194,17 @@ class BuildBackend(LoggingMixin):
         Example usage:
 
             with self._write_file('foo.txt') as fh:
                 fh.write('hello world')
         """
 
         if path is not None:
             assert fh is None
-            fh = FileAvoidWrite(path, capture_diff=True)
+            fh = FileAvoidWrite(path, capture_diff=True, dry_run=self.dry_run)
         else:
             assert fh is not None
 
         dirname = mozpath.dirname(fh.name)
         try:
             os.makedirs(dirname)
         except OSError as error:
             if error.errno != errno.EEXIST:
--- a/python/mozbuild/mozbuild/config_status.py
+++ b/python/mozbuild/mozbuild/config_status.py
@@ -111,16 +111,18 @@ def config_status(topobjdir='.', topsrcd
     parser.add_argument('-n', dest='not_topobjdir', action='store_true',
                         help='do not consider current directory as top object directory')
     parser.add_argument('-d', '--diff', action='store_true',
                         help='print diffs of changed files.')
     parser.add_argument('-b', '--backend', nargs='+', choices=sorted(backends),
                         default=default_backends,
                         help='what backend to build (default: %s).' %
                         ' '.join(default_backends))
+    parser.add_argument('--dry-run', action='store_true',
+                        help='do everything except writing files out.')
     options = parser.parse_args()
 
     # Without -n, the current directory is meant to be the top object directory
     if not options.not_topobjdir:
         topobjdir = os.path.abspath('.')
 
     env = ConfigEnvironment(topsrcdir, topobjdir, defines=defines,
             non_global_defines=non_global_defines, substs=substs, source=source)
@@ -132,16 +134,20 @@ def config_status(topobjdir='.', topsrcd
 
     cpu_start = time.clock()
     time_start = time.time()
 
     # Make appropriate backend instances, defaulting to RecursiveMakeBackend,
     # or what is in BUILD_BACKENDS.
     selected_backends = [get_backend_class(b)(env) for b in options.backend]
 
+    if options.dry_run:
+        for b in selected_backends:
+            b.dry_run = True
+
     reader = BuildReader(env)
     emitter = TreeMetadataEmitter(env)
     # This won't actually do anything because of the magic of generators.
     definitions = emitter.emit(reader.read_topsrcdir())
 
     if options.recheck:
         # Execute configure from the top object directory
         os.chdir(topobjdir)
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -602,17 +602,19 @@ class Build(MachCommandBase):
     @CommandArgument('-d', '--diff', action='store_true',
         help='Show a diff of changes.')
     # It would be nice to filter the choices below based on
     # conditions, but that is for another day.
     @CommandArgument('-b', '--backend', nargs='+', choices=sorted(backends),
         help='Which backend to build.')
     @CommandArgument('-v', '--verbose', action='store_true',
         help='Verbose output.')
-    def build_backend(self, backend, diff=False, verbose=False):
+    @CommandArgument('-n', '--dry-run', action='store_true',
+        help='Do everything except writing files out.')
+    def build_backend(self, backend, diff=False, verbose=False, dry_run=False):
         python = self.virtualenv_manager.python_path
         config_status = os.path.join(self.topobjdir, 'config.status')
 
         if not os.path.exists(config_status):
             print('config.status not found.  Please run |mach configure| '
                   'or |mach build| prior to building the %s build backend.'
                   % backend)
             return 1
@@ -620,16 +622,18 @@ class Build(MachCommandBase):
         args = [python, config_status]
         if backend:
             args.append('--backend')
             args.extend(backend)
         if diff:
             args.append('--diff')
         if verbose:
             args.append('--verbose')
+        if dry_run:
+            args.append('--dry-run')
 
         return self._run_command_in_objdir(args=args, pass_thru=True,
             ensure_exit_code=False)
 
     def _run_mach_artifact_install(self):
         # We'd like to launch artifact using
         # self._mach_context.commands.dispatch.  However, artifact activates
         # the virtualenv, which plays badly with the rest of this code.
--- a/python/mozbuild/mozbuild/util.py
+++ b/python/mozbuild/mozbuild/util.py
@@ -134,21 +134,26 @@ class FileAvoidWrite(BytesIO):
     We create an instance from an existing filename. New content is written to
     it. When we close the file object, if the content in the in-memory buffer
     differs from what is on disk, then we write out the new content. Otherwise,
     the original file is untouched.
 
     Instances can optionally capture diffs of file changes. This feature is not
     enabled by default because it a) doesn't make sense for binary files b)
     could add unwanted overhead to calls.
+
+    Additionally, there is dry run mode where the file is not actually written
+    out, but reports whether the file was existing and would have been updated
+    still occur, as well as diff capture if requested.
     """
-    def __init__(self, filename, capture_diff=False, mode='rU'):
+    def __init__(self, filename, capture_diff=False, dry_run=False, mode='rU'):
         BytesIO.__init__(self)
         self.name = filename
         self._capture_diff = capture_diff
+        self._dry_run = dry_run
         self.diff = None
         self.mode = mode
 
     def write(self, buf):
         if isinstance(buf, unicode):
             buf = buf.encode('utf-8')
         BytesIO.write(self, buf)
 
@@ -178,24 +183,25 @@ class FileAvoidWrite(BytesIO):
                 old_content = existing.read()
                 if old_content == buf:
                     return True, False
             except IOError:
                 pass
             finally:
                 existing.close()
 
-        ensureParentDir(self.name)
-        # Maintain 'b' if specified.  'U' only applies to modes starting with
-        # 'r', so it is dropped.
-        writemode = 'w'
-        if 'b' in self.mode:
-            writemode += 'b'
-        with open(self.name, writemode) as file:
-            file.write(buf)
+        if not self._dry_run:
+            ensureParentDir(self.name)
+            # Maintain 'b' if specified.  'U' only applies to modes starting with
+            # 'r', so it is dropped.
+            writemode = 'w'
+            if 'b' in self.mode:
+                writemode += 'b'
+            with open(self.name, writemode) as file:
+                file.write(buf)
 
         if self._capture_diff:
             try:
                 old_lines = old_content.splitlines() if existed else None
                 new_lines = buf.splitlines()
 
                 self.diff = simple_diff(self.name, old_lines, new_lines)
             # FileAvoidWrite isn't unicode/bytes safe. So, files with non-ascii