OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright 2015 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """Checks that released mojom.dart files in the source tree are up to date""" |
| 7 |
| 8 import argparse |
| 9 import os |
| 10 import subprocess |
| 11 import sys |
| 12 |
| 13 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 14 SRC_DIR = os.path.dirname( |
| 15 os.path.dirname( |
| 16 os.path.dirname( |
| 17 os.path.dirname(SCRIPT_DIR)))) |
| 18 |
| 19 # Insert path to mojom parsing library. |
| 20 sys.path.insert(0, os.path.join(SRC_DIR, |
| 21 'mojo', |
| 22 'public', |
| 23 'tools', |
| 24 'bindings', |
| 25 'pylib')) |
| 26 |
| 27 from mojom.error import Error |
| 28 from mojom.parse.parser import Parse |
| 29 from mojom.parse.translate import Translate |
| 30 |
| 31 PACKAGES_DIR = os.path.join(SRC_DIR, 'mojo', 'dart', 'packages') |
| 32 |
| 33 # Script that calculates mojom output paths. |
| 34 DART_OUTPUTS_SCRIPT = os.path.join(SRC_DIR, |
| 35 'mojo', |
| 36 'public', |
| 37 'tools', |
| 38 'bindings', |
| 39 'mojom_list_dart_outputs.py') |
| 40 |
| 41 # Runs command line in args from cwd. Returns the output as a string. |
| 42 def run(cwd, args): |
| 43 return subprocess.check_output(args, cwd=cwd) |
| 44 |
| 45 |
| 46 # Given a parsed mojom, return the path of the .mojom.dart relative to its |
| 47 # package directory. |
| 48 def _mojom_output_path(mojom): |
| 49 name = mojom['name'] |
| 50 namespace = mojom['namespace'] |
| 51 elements = ['lib'] |
| 52 elements.extend(namespace.split('.')) |
| 53 elements.append("%s.dart" % name) |
| 54 return os.path.join(*elements) |
| 55 |
| 56 |
| 57 # Given a parsed mojom, return the package or None. |
| 58 def _mojom_package(mojom): |
| 59 attributes = mojom['attributes'] |
| 60 return attributes['DartPackage'] |
| 61 |
| 62 |
| 63 # Load and parse a .mojom file. Returns the parsed mojom. |
| 64 def _load_mojom(path_to_mojom): |
| 65 filename = os.path.abspath(path_to_mojom) |
| 66 name = os.path.basename(filename) |
| 67 |
| 68 # Read in mojom file. |
| 69 with open(filename) as f: |
| 70 source = f.read() |
| 71 # Parse |
| 72 tree = Parse(source, name) |
| 73 mojom = Translate(tree, name) |
| 74 return mojom |
| 75 |
| 76 |
| 77 def _print_regenerate_message(package): |
| 78 print(""" |
| 79 *** Dart Generated Bindings Check Failed for package: %s |
| 80 |
| 81 To regenerate bindings, from the src directory, run: |
| 82 ./third_party/dart-sdk/dart-sdk/bin/dart mojo/dart/packages/mojom/bin/mojom.dart
gen -m mojo/public -r mojo/ --output mojo/dart/packages/ |
| 83 """ % (package)) |
| 84 |
| 85 |
| 86 # Returns a map from package name to source directory. |
| 87 def _build_package_map(): |
| 88 packages = {} |
| 89 for package in os.listdir(PACKAGES_DIR): |
| 90 package_path = os.path.join(PACKAGES_DIR, package) |
| 91 # Skip everything but directories. |
| 92 if not os.path.isdir(package_path): |
| 93 continue |
| 94 packages[package] = package_path |
| 95 return packages |
| 96 |
| 97 |
| 98 # Returns a list of paths to .mojom files vended by package_name. |
| 99 def _find_mojoms_for_package(package_name): |
| 100 # Run git grep for all .mojom files with DartPackage="package_name" |
| 101 try: |
| 102 output = run(SRC_DIR, ['git', |
| 103 'grep', |
| 104 '--name-only', |
| 105 'DartPackage="' + package_name + '"', |
| 106 '--', |
| 107 '*.mojom']) |
| 108 except subprocess.CalledProcessError as e: |
| 109 # git grep exits with code 1 if nothing was found. |
| 110 if e.returncode == 1: |
| 111 return [] |
| 112 |
| 113 # Process output |
| 114 mojoms = [] |
| 115 for line in output.splitlines(): |
| 116 line = line.strip() |
| 117 # Skip empty lines. |
| 118 if not line: |
| 119 continue |
| 120 mojoms.append(line) |
| 121 return mojoms |
| 122 |
| 123 |
| 124 # Return the list of expected mojom.dart files for a package. |
| 125 def _expected_mojom_darts_for_package(mojoms): |
| 126 output = run(SRC_DIR, ['python', |
| 127 DART_OUTPUTS_SCRIPT, |
| 128 '--mojoms'] + mojoms) |
| 129 mojom_darts = [] |
| 130 for line in output.splitlines(): |
| 131 line = line.strip() |
| 132 # Skip empty lines. |
| 133 if not line: |
| 134 continue |
| 135 mojom_darts.append(line) |
| 136 return mojom_darts |
| 137 |
| 138 |
| 139 # Returns a map indexed by output mojom.dart name with the value of |
| 140 # the modification time of the .mojom file in the source tree. |
| 141 def _build_expected_map(mojoms, mojom_darts): |
| 142 assert(len(mojom_darts) == len(mojoms)) |
| 143 expected = {} |
| 144 for i in range(0, len(mojoms)): |
| 145 mojom_path = os.path.join(SRC_DIR, mojoms[i]) |
| 146 expected[mojom_darts[i]] = os.path.getmtime(mojom_path) |
| 147 return expected |
| 148 |
| 149 |
| 150 # Returns a map indexed by output mojom.dart name with the value of |
| 151 # the modification time of the .mojom.dart file in the source tree. |
| 152 def _build_current_map(package): |
| 153 current = {} |
| 154 package_path = os.path.join(PACKAGES_DIR, package) |
| 155 for directory, _, files in os.walk(package_path): |
| 156 for filename in files: |
| 157 if filename.endswith('.mojom.dart'): |
| 158 path = os.path.abspath(os.path.join(directory, filename)) |
| 159 relpath = os.path.relpath(path, start=PACKAGES_DIR) |
| 160 current[relpath] = os.path.getmtime(path) |
| 161 return current |
| 162 |
| 163 |
| 164 # Checks if a mojom.dart file we expected in the source tree isn't there. |
| 165 def _check_new(package, expected, current): |
| 166 check_failure = False |
| 167 for mojom_dart in expected: |
| 168 if not current.get(mojom_dart): |
| 169 print("Package %s missing %s" % (package, mojom_dart)) |
| 170 check_failure = True |
| 171 return check_failure |
| 172 |
| 173 |
| 174 # Checks if a mojom.dart file exists without an associated .mojom file. |
| 175 def _check_delete(package, expected, current): |
| 176 check_failure = False |
| 177 for mojom_dart in current: |
| 178 if not expected.get(mojom_dart): |
| 179 print("Package %s no longer has %s." % (package, mojom_dart)) |
| 180 print("Delete %s", os.path.join(PACKAGES_DIR, mojom_dart)) |
| 181 check_failure = True |
| 182 return check_failure |
| 183 |
| 184 |
| 185 # Checks if a .mojom.dart file is older than the associated .mojom file. |
| 186 # TODO(johnmccutchan): Handle the case where someone edited a .mojom.dart file |
| 187 # directly instead of through the bindings generation script. |
| 188 def _check_stale(package, expected, current): |
| 189 check_failure = False |
| 190 for mojom_dart in expected: |
| 191 # Missing mojom.dart file in source tree case handled by _check_new. |
| 192 source_mtime = expected[mojom_dart] |
| 193 if not current.get(mojom_dart): |
| 194 continue |
| 195 generated_mtime = current[mojom_dart] |
| 196 if generated_mtime < source_mtime: |
| 197 print("Package %s has old %s" % (package, mojom_dart)) |
| 198 check_failure = True |
| 199 return check_failure |
| 200 |
| 201 |
| 202 # Returns True if any checks fail. |
| 203 def _check(package, expected, current): |
| 204 check_failure = False |
| 205 if _check_new(package, expected, current): |
| 206 check_failure = True |
| 207 if _check_stale(package, expected, current): |
| 208 check_failure = True |
| 209 if _check_delete(package, expected, current): |
| 210 check_failure = True |
| 211 return check_failure |
| 212 |
| 213 |
| 214 def global_check(packages): |
| 215 check_failure = False |
| 216 for package in packages: |
| 217 mojoms = _find_mojoms_for_package(package) |
| 218 if not mojoms: |
| 219 continue |
| 220 mojom_darts = _expected_mojom_darts_for_package(mojoms) |
| 221 # We only feed in mojom files with DartPackage annotations, therefore, we |
| 222 # should have a 1:1 mapping from mojoms[i] to mojom_darts[i]. |
| 223 assert(len(mojom_darts) == len(mojoms)) |
| 224 expected = _build_expected_map(mojoms, mojom_darts) |
| 225 current = _build_current_map(package) |
| 226 if _check(package, expected, current): |
| 227 _print_regenerate_message(package) |
| 228 check_failure = True |
| 229 return check_failure |
| 230 |
| 231 |
| 232 def is_mojom_dart(path): |
| 233 return path.endswith('.mojom.dart') |
| 234 |
| 235 |
| 236 def is_mojom(path): |
| 237 return path.endswith('.mojom') |
| 238 |
| 239 |
| 240 def filter_paths(paths, path_filter): |
| 241 result = [] |
| 242 for path in paths: |
| 243 path = os.path.abspath(os.path.join(SRC_DIR, path)) |
| 244 if path_filter(path): |
| 245 result.append(path) |
| 246 return result |
| 247 |
| 248 |
| 249 def safe_mtime(path): |
| 250 try: |
| 251 return os.path.getmtime(path) |
| 252 except Exception: |
| 253 pass |
| 254 return 0 |
| 255 |
| 256 |
| 257 def presubmit_check(packages, affected_files): |
| 258 mojoms = filter_paths(affected_files, is_mojom) |
| 259 mojom_darts = filter_paths(affected_files, is_mojom_dart) |
| 260 updated_mojom_dart_files = [] |
| 261 packages_with_failures = [] |
| 262 check_failure = False |
| 263 |
| 264 # Check for updated .mojom without updated .mojom.dart |
| 265 for mojom_file in mojoms: |
| 266 try: |
| 267 mojom = _load_mojom(mojom_file) |
| 268 except Exception: |
| 269 # Could not load .mojom file |
| 270 return True |
| 271 |
| 272 package = _mojom_package(mojom) |
| 273 # If a mojom doesn't have a package, ignore it. |
| 274 if not package: |
| 275 continue |
| 276 package_dir = packages.get(package) |
| 277 # If the package isn't a known package, ignore it. |
| 278 if not package_dir: |
| 279 continue |
| 280 # Expected output path relative to src. |
| 281 mojom_dart_path = os.path.relpath( |
| 282 os.path.join(package_dir, _mojom_output_path(mojom)), start=SRC_DIR) |
| 283 |
| 284 mojom_mtime = safe_mtime(mojom_file) |
| 285 mojom_dart_mtime = safe_mtime(os.path.join(SRC_DIR, mojom_dart_path)) |
| 286 |
| 287 if mojom_mtime > mojom_dart_mtime: |
| 288 check_failure = True |
| 289 print("Package %s has old %s" % (package, mojom_dart_path)) |
| 290 if not (package in packages_with_failures): |
| 291 packages_with_failures.append(package) |
| 292 continue |
| 293 # Remember that this .mojom.dart file was updated after the .mojom file. |
| 294 # This list is used to verify that all updated .mojom.dart files were |
| 295 # updated because their source .mojom file changed. |
| 296 updated_mojom_dart_files.append(mojom_dart_path) |
| 297 |
| 298 # Check for updated .mojom.dart file without updated .mojom file. |
| 299 for mojom_dart_file in mojom_darts: |
| 300 # mojom_dart_file is not inside //mojo/dart/packages. |
| 301 if not mojom_dart_file.startswith(PACKAGES_DIR): |
| 302 continue |
| 303 |
| 304 # Path relative to //mojo/dart/packages/ |
| 305 path_relative_to_packages = os.path.relpath(mojom_dart_file, |
| 306 start=PACKAGES_DIR) |
| 307 # Package name is first element of split path. |
| 308 package = path_relative_to_packages.split(os.sep)[0] |
| 309 # Path relative to src. |
| 310 mojom_dart_path = os.path.relpath(mojom_dart_file, start=SRC_DIR) |
| 311 # If mojom_dart_path is not in updated_mojom_dart_files, a .mojom.dart |
| 312 # file was updated without updating the related .mojom file. |
| 313 if not (mojom_dart_path in updated_mojom_dart_files): |
| 314 check_failure = True |
| 315 print("Package %s has new %s without updating source .mojom file." % |
| 316 (package, mojom_dart_path)) |
| 317 if not (package in packages_with_failures): |
| 318 packages_with_failures.append(package) |
| 319 |
| 320 for package in packages_with_failures: |
| 321 _print_regenerate_message(package) |
| 322 |
| 323 return check_failure |
| 324 |
| 325 |
| 326 def main(): |
| 327 parser = argparse.ArgumentParser(description='Generate a dart-pkg') |
| 328 parser.add_argument('--affected-files', |
| 329 action='store', |
| 330 metavar='affected_files', |
| 331 help='List of files that should be checked.', |
| 332 nargs='+') |
| 333 args = parser.parse_args() |
| 334 packages = _build_package_map() |
| 335 |
| 336 # This script runs in two modes, the first mode is invoked by PRESUBMIT.py |
| 337 # and passes the list of affected files. This checks for the following cases: |
| 338 # 1) An updated .mojom file without an updated .mojom.dart file. |
| 339 # 2) An updated .mojom.dart file without an updated .mojom file. |
| 340 # NOTE: Case 1) also handles the case of a new .mojom file being added. |
| 341 # |
| 342 # The second mode does a global check of all packages under |
| 343 # //mojo/dart/packages. This checks for the following cases: |
| 344 # 1) An updated .mojom file without an updated .mojom.dart file. |
| 345 # 2) A .mojom.dart file without an associated .mojom file (deletion case). |
| 346 if args.affected_files: |
| 347 check_failure = presubmit_check(packages, args.affected_files) |
| 348 else: |
| 349 check_failure = global_check(packages) |
| 350 if check_failure: |
| 351 return 2 |
| 352 return 0 |
| 353 |
| 354 if __name__ == '__main__': |
| 355 sys.exit(main()) |
OLD | NEW |