PRESUBMIT: Improve --check-only logic in tools/gen_xxx

This change introduces more uniform and reliable
handling of the presubmit-time check of generators.
Instead of delegating the check to the PRESUBMIT.py
script, it introduce a --check-only option.
Then it changes the behavior of generator to:
- Always write to xxx.swp files (which are .gitignore-d)
- If --check-only, diff-test and remove them
- If not, rename them into the final location

Test: manual (edit files, run git cl presubmit --force)
Change-Id: Ic156620c591bd434781c543803c9ab7aa3c2a33c
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index e2dd762..db40ea5 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -51,57 +51,30 @@
 
 
 def CheckBuild(input_api, output_api):
+  tool = 'tools/gen_bazel'
   # If no GN files were modified, bail out.
   def build_file_filter(x): return input_api.FilterSourceFile(
       x,
-      white_list=('.*BUILD[.]gn$', '.*[.]gni$', 'tools/gen_bazel',
-          'BUILD\.extras'))
+      white_list=('.*BUILD[.]gn$', '.*[.]gni$', 'BUILD\.extras', tool))
   if not input_api.AffectedSourceFiles(build_file_filter):
     return []
-
-  with open('BUILD') as f:
-    current_build = f.read()
-
-  new_build = subprocess.check_output(
-      ['tools/gen_bazel', '--output', '/dev/stdout', '--output-proto',
-       '/dev/null'])
-
-  with open('protos/BUILD') as f:
-    current_proto_build = f.read()
-
-  new_proto_build = subprocess.check_output(
-      ['tools/gen_bazel', '--output', '/dev/null', '--output-proto',
-       '/dev/stdout'])
-
-  if current_build != new_build or current_proto_build != new_proto_build:
-    return [
-        output_api.PresubmitError(
-            'BUILD and/or protos/BUILD is out of date. ' +
-            'Please run tools/gen_bazel to update it.')
-    ]
+  if subprocess.call([tool, '--check-only']):
+    return [output_api.PresubmitError('Bazel BUILD(s) are out of date. Run '
+            + tool + ' to update them.')]
   return []
 
 
 def CheckAndroidBlueprint(input_api, output_api):
+  tool = 'tools/gen_android_bp'
   # If no GN files were modified, bail out.
   def build_file_filter(x): return input_api.FilterSourceFile(
       x,
-      white_list=('.*BUILD[.]gn$', '.*[.]gni$', 'tools/gen_android_bp'))
+      white_list=('.*BUILD[.]gn$', '.*[.]gni$', tool))
   if not input_api.AffectedSourceFiles(build_file_filter):
     return []
-
-  with open('Android.bp') as f:
-    current_blueprint = f.read()
-
-  new_blueprint = subprocess.check_output(
-      ['tools/gen_android_bp', '--output', '/dev/stdout'])
-
-  if current_blueprint != new_blueprint:
-    return [
-        output_api.PresubmitError(
-            'Android.bp is out of date. Please run tools/gen_android_bp '
-            'to update it.')
-    ]
+  if subprocess.call([tool, '--check-only']):
+    return [output_api.PresubmitError('Android build files are out of date. ' +
+            'Run ' + tool + ' to update them.')]
   return []
 
 
diff --git a/tools/gen_amalgamated b/tools/gen_amalgamated
index 06da7a3..a8d40da 100755
--- a/tools/gen_amalgamated
+++ b/tools/gen_amalgamated
@@ -44,7 +44,11 @@
 
 # Arguments for the GN output directory (unless overridden from the command
 # line).
-gn_args = 'is_debug=false perfetto_enable_ipc=true'
+gn_args = ' '.join([
+    'is_debug=false',
+    'perfetto_build_with_embedder=true',
+    'perfetto_enable_ipc=true'
+])
 
 # Compiler flags which aren't filtered out.
 cflag_whitelist = r'^-(W.*|fno-exceptions|fPIC|std.*|fvisibility.*)$'
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index aa13178..dda247a 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -72,7 +72,12 @@
 ]
 
 # Arguments for the GN output directory.
-gn_args = 'target_os="android" target_cpu="arm" is_debug=false perfetto_build_with_android=true'
+gn_args = ' '.join([
+    'is_debug=false',
+    'perfetto_build_with_android=true',
+    'target_cpu="arm"',
+    'target_os="android"',
+])
 
 # All module names are prefixed with this string to avoid collisions.
 module_prefix = 'perfetto_'
@@ -721,6 +726,9 @@
     parser = argparse.ArgumentParser(
         description='Generate Android.bp from a GN description.')
     parser.add_argument(
+        '--check-only', help='Don\'t keep the generated files',
+        action='store_true')
+    parser.add_argument(
         '--desc',
         help=
         'GN description (e.g., gn desc out --format=json --all-toolchains "//*"'
@@ -771,9 +779,18 @@
     with open(args.extras, 'r') as r:
         for line in r:
             output.append(line.rstrip("\n\r"))
-    with open(args.output, 'w') as f:
+
+    out_files = []
+
+    # Generate the Android.bp file.
+    out_files.append(args.output + '.swp')
+    with open(out_files[-1], 'w') as f:
         f.write('\n'.join(output))
 
 
+    # Either check the contents or move the files to their final destination.
+    return gn_utils.check_or_commit_generated_files (out_files, args.check_only)
+
+
 if __name__ == '__main__':
     sys.exit(main())
diff --git a/tools/gen_bazel b/tools/gen_bazel
index 08a0f6a..4c57bf3 100755
--- a/tools/gen_bazel
+++ b/tools/gen_bazel
@@ -54,7 +54,11 @@
 
 # Arguments for the GN output directory.
 # host_os="linux" is to generate the right build files from Mac OS.
-gn_args = 'target_os="linux" is_debug=false host_os="linux"'
+gn_args = ' '.join([
+    'host_os="linux"',
+    'is_debug=false',
+    'target_os="linux"',
+])
 
 # Default targets to translate to the blueprint file.
 default_targets = [
@@ -69,7 +73,7 @@
   '//protos/perfetto/metrics:lite_gen'
 ]
 
-# Aliases to add to the BUILD file
+# Aliases to add to the BUILD file.
 alias_targets = {
   '//src/protozero:libprotozero': 'libprotozero',
   '//src/trace_processor:trace_processor': 'trace_processor',
@@ -606,6 +610,9 @@
   parser = argparse.ArgumentParser(
       description='Generate BUILD from a GN description.')
   parser.add_argument(
+        '--check-only', help='Don\'t keep the generated files',
+        action='store_true')
+  parser.add_argument(
       '--desc',
       help='GN description (e.g., gn desc out --format=json --all-toolchains "//*"'
   )
@@ -641,22 +648,28 @@
   else:
     desc = gn_utils.create_build_description(gn_args, args.repo_root)
 
+  out_files = []
+
   build_generator = BuildGenerator(desc)
   build, proto_build = build_generator.create_build_for_targets(
       args.targets or default_targets)
-  with open(args.output, 'w') as f:
+
+  # Generate the main BUILD file.
+  out_files.append(args.output + '.swp')
+  with open(out_files[-1], 'w') as f:
     writer = Writer(f)
     build.write(writer)
     writer.newline()
-
     with open(args.extras, 'r') as r:
       for line in r:
         writer.line(line.rstrip("\n\r"))
 
-  with open(args.output_proto, 'w') as f:
+  # Generate the protos/BUILD file.
+  out_files.append(args.output_proto + '.swp')
+  with open(out_files[-1], 'w') as f:
     proto_build.write(Writer(f))
 
-  return 0
+  return gn_utils.check_or_commit_generated_files(out_files, args.check_only)
 
 
 if __name__ == '__main__':
diff --git a/tools/gn_utils.py b/tools/gn_utils.py
index 5c6d318..ca3e003 100644
--- a/tools/gn_utils.py
+++ b/tools/gn_utils.py
@@ -17,6 +17,7 @@
 
 from __future__ import print_function
 import errno
+import filecmp
 import json
 import os
 import re
@@ -86,14 +87,17 @@
         shutil.rmtree(out)
 
 
-def build_targets(out, targets):
+def build_targets(out, targets, quiet=False):
     """Runs ninja to build a list of GN targets in the given out directory.
 
     Compiling these targets is required so that we can include any generated
     source files in the amalgamated result.
     """
     targets = [t.replace('//', '') for t in targets]
-    subprocess.check_call([_tool_path('ninja')] + targets, cwd=out)
+    with open(os.devnull, 'rw') as devnull:
+        stdout = devnull if quiet else None
+        subprocess.check_call([_tool_path('ninja')] + targets, cwd=out,
+                              stdout=stdout)
 
 
 def compute_source_dependencies(out):
@@ -141,4 +145,25 @@
   """
   name = re.sub(r'^//:?', '', label)
   name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
-  return name
\ No newline at end of file
+  return name
+
+def check_or_commit_generated_files(tmp_files, check):
+    """Checks that gen files are unchanged or renames them to the final location
+
+    Takes in input a list of 'xxx.swp' files that have been written.
+    If check == False, it renames xxx.swp -> xxx.
+    If check == True, it just checks that the contents of 'xxx.swp' == 'xxx'.
+    Returns 0 if no diff was detected, 1 otherwise (to be used as exit code).
+    """
+    res = 0
+    for tmp_file in tmp_files:
+        assert(tmp_file.endswith('.swp'))
+        target_file = os.path.relpath(tmp_file[:-4])
+        if check:
+            if not filecmp.cmp(tmp_file, target_file):
+                sys.stderr.write('%s needs to be regenerated\n' % target_file)
+                res = 1
+            os.unlink(tmp_file)
+        else:
+            os.rename(tmp_file, target_file)
+    return res
\ No newline at end of file