building.py (66583B)
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 5 import errno 6 import getpass 7 import io 8 import json 9 import logging 10 import os 11 import re 12 import subprocess 13 import sys 14 import time 15 from collections import Counter, OrderedDict, namedtuple 16 from itertools import dropwhile, islice, takewhile 17 from textwrap import TextWrapper 18 19 from mach.site import CommandSiteManager 20 21 try: 22 import psutil 23 except Exception: 24 psutil = None 25 26 import mozfile 27 import mozpack.path as mozpath 28 from mach.mixin.logging import LoggingMixin 29 from mach.util import get_state_dir, get_virtualenv_base_dir 30 from mozsystemmonitor.resourcemonitor import SystemResourceMonitor 31 from mozterm.widgets import Footer 32 33 from ..backend import get_backend_class 34 from ..base import MozbuildObject 35 from ..compilation.warnings import WarningsCollector, WarningsDatabase 36 from ..dirutils import mkdir 37 from ..serialized_logging import read_serialized_record 38 from ..telemetry import get_cpu_brand 39 from ..testing import install_test_files 40 from ..util import FileAvoidWrite, resolve_target_to_make 41 from .clobber import Clobberer 42 43 FINDER_SLOW_MESSAGE = """ 44 =================== 45 PERFORMANCE WARNING 46 47 The OS X Finder application (file indexing used by Spotlight) used a lot of CPU 48 during the build - an average of %f%% (100%% is 1 core). This made your build 49 slower. 50 51 Consider adding ".noindex" to the end of your object directory name to have 52 Finder ignore it. Or, add an indexing exclusion through the Spotlight System 53 Preferences. 54 =================== 55 """.strip() 56 57 58 INSTALL_TESTS_CLOBBER = "".join([ 59 TextWrapper().fill(line) + "\n" 60 for line in """ 61 The build system was unable to install tests because the CLOBBER file has \ 62 been updated. This means if you edited any test files, your changes may not \ 63 be picked up until a full/clobber build is performed. 64 65 The easiest and fastest way to perform a clobber build is to run: 66 67 $ mach clobber 68 $ mach build 69 70 If you did not modify any test files, it is safe to ignore this message \ 71 and proceed with running tests. To do this run: 72 73 $ touch {clobber_file} 74 """.splitlines() 75 ]) 76 77 CLOBBER_REQUESTED_MESSAGE = """ 78 =================== 79 The CLOBBER file was updated prior to this build. A clobber build may be 80 required to succeed, but we weren't expecting it to. 81 82 Please consider filing a bug for this failure if you have reason to believe 83 this is a clobber bug and not due to local changes. 84 =================== 85 """.strip() 86 87 88 BuildOutputResult = namedtuple( 89 "BuildOutputResult", ("warning", "state_changed", "message") 90 ) 91 92 93 class TierStatus: 94 """Represents the state and progress of tier traversal. 95 96 The build system is organized into linear phases called tiers. Each tier 97 executes in the order it was defined, 1 at a time. 98 """ 99 100 def __init__(self, resources, metrics): 101 """Accepts a SystemResourceMonitor to record results against.""" 102 self.tiers = OrderedDict() 103 self.tier_status = OrderedDict() 104 self.resources = resources 105 self.metrics = metrics 106 107 def set_tiers(self, tiers): 108 """Record the set of known tiers.""" 109 for tier in tiers: 110 self.tiers[tier] = dict( 111 begin_time=None, 112 finish_time=None, 113 duration=None, 114 ) 115 self.tier_status[tier] = None 116 117 def begin_tier(self, tier): 118 """Record that execution of a tier has begun.""" 119 self.tier_status[tier] = "active" 120 t = self.tiers[tier] 121 t["begin_time"] = time.monotonic() 122 self.resources.begin_phase(tier) 123 metrics_tier_name = "tier_" + tier.replace("-", "_") + "_duration" 124 metrics_attribute = getattr(self.metrics.mozbuild, metrics_tier_name, None) 125 if metrics_attribute: 126 metrics_attribute.start() 127 128 def finish_tier(self, tier): 129 """Record that execution of a tier has finished.""" 130 self.tier_status[tier] = "finished" 131 t = self.tiers[tier] 132 t["finish_time"] = time.monotonic() 133 t["duration"] = self.resources.finish_phase(tier) 134 metrics_tier_name = "tier_" + tier.replace("-", "_") + "_duration" 135 metrics_attribute = getattr(self.metrics.mozbuild, metrics_tier_name, None) 136 if metrics_attribute: 137 metrics_attribute.stop() 138 139 140 def record_cargo_timings(resource_monitor, timings_path): 141 cargo_start = 0 142 try: 143 with open(timings_path) as fh: 144 # Extrace the UNIT_DATA list from the cargo timing HTML file. 145 unit_data = dropwhile(lambda l: l.rstrip() != "const UNIT_DATA = [", fh) 146 unit_data = islice(unit_data, 1, None) 147 lines = takewhile(lambda l: l.rstrip() != "];", unit_data) 148 entries = json.loads("[" + "".join(lines) + "]") 149 # Normalize the entries so that any change in data format would 150 # trigger the exception handler that skips this (we don't want the 151 # build to fail in that case) 152 data = [ 153 ( 154 "{} v{}{}".format( 155 entry["name"], entry["version"], entry.get("target", "") 156 ), 157 entry["start"] or 0, 158 entry["duration"] or 0, 159 ) 160 for entry in entries 161 ] 162 starts = [ 163 start 164 for marker, start in resource_monitor._active_markers.items() 165 if marker.startswith("Rust:") 166 ] 167 # The build system is not supposed to be running more than one cargo 168 # at the same time, which thankfully makes it easier to find the start 169 # of the one we got the timings for. 170 if len(starts) != 1: 171 return 172 cargo_start = starts[0] 173 except Exception: 174 return 175 176 if not cargo_start: 177 return 178 179 for name, start, duration in data: 180 resource_monitor.record_marker( 181 "RustCrate", cargo_start + start, cargo_start + start + duration, name 182 ) 183 184 185 class BuildMonitor(MozbuildObject): 186 """Monitors the output of the build.""" 187 188 def init(self, warnings_path, terminal, metrics): 189 """Create a new monitor. 190 191 warnings_path is a path of a warnings database to use. 192 """ 193 self._warnings_path = warnings_path 194 self.resources = SystemResourceMonitor( 195 poll_interval=0.1, 196 metadata={"CPUName": get_cpu_brand()}, 197 ) 198 self._resources_started = False 199 200 self.tiers = TierStatus(self.resources, metrics) 201 202 self.warnings_database = WarningsDatabase() 203 if os.path.exists(warnings_path): 204 try: 205 self.warnings_database.load_from_file(warnings_path) 206 except ValueError: 207 os.remove(warnings_path) 208 209 # Contains warnings unique to this invocation. Not populated with old 210 # warnings. 211 self.instance_warnings = WarningsDatabase() 212 213 self._terminal = terminal 214 215 def on_warning(warning): 216 # Skip `errors` 217 if warning["type"] == "error": 218 return 219 220 filename = warning["filename"] 221 222 if not os.path.exists(filename): 223 raise Exception("Could not find file containing warning: %s" % filename) 224 225 self.warnings_database.insert(warning) 226 # Make a copy so mutations don't impact other database. 227 self.instance_warnings.insert(warning.copy()) 228 229 self._warnings_collector = WarningsCollector(on_warning, objdir=self.topobjdir) 230 231 self.build_objects = [] 232 self.build_dirs = set() 233 234 def start(self): 235 """Record the start of the build.""" 236 self.start_time = time.monotonic() 237 self._finder_start_cpu = self._get_finder_cpu_usage() 238 239 def start_resource_recording(self): 240 # This should be merged into start() once bug 892342 lands. 241 self.resources.start() 242 self._resources_started = True 243 244 def on_line(self, line): 245 """Consume a line of output from the build system. 246 247 This will parse the line for state and determine whether more action is 248 needed. 249 250 Returns a BuildOutputResult instance. 251 252 In this named tuple, warning will be an object describing a new parsed 253 warning. Otherwise it will be None. 254 255 state_changed indicates whether the build system changed state with 256 this line. If the build system changed state, the caller may want to 257 query this instance for the current state in order to update UI, etc. 258 259 message is either None, or the content of a message to be 260 displayed to the user (as a str or a logging.LogRecord). 261 """ 262 message = None 263 264 # If the previous line was colored (eg. for a compiler warning), our 265 # line will start with the ansi reset sequence. Strip it to ensure it 266 # does not interfere with our parsing of the line. 267 plain_line = self._terminal.strip(line) if self._terminal else line.strip() 268 if plain_line.startswith("BUILDSTATUS"): 269 args = plain_line.split() 270 271 _, _, disambiguator = args.pop(0).partition("@") 272 action = args.pop(0) 273 time = None 274 regexp = re.compile(r"\d{10}(\.\d{1,9})?$") 275 if regexp.match(action): 276 time = float(action) 277 action = args.pop(0) 278 279 update_needed = True 280 281 if action == "TIERS": 282 self.tiers.set_tiers(args) 283 update_needed = False 284 elif action == "TIER_START": 285 tier = args[0] 286 self.tiers.begin_tier(tier) 287 elif action == "TIER_FINISH": 288 (tier,) = args 289 self.tiers.finish_tier(tier) 290 elif action == "OBJECT_FILE": 291 self.build_objects.append(args[0]) 292 self.resources.begin_marker("Object", args[0], disambiguator) 293 update_needed = False 294 elif action.startswith("START_"): 295 self.resources.begin_marker( 296 action[len("START_") :], " ".join(args), disambiguator, time 297 ) 298 update_needed = False 299 elif action.startswith("END_"): 300 self.resources.end_marker( 301 action[len("END_") :], " ".join(args), disambiguator, time 302 ) 303 update_needed = False 304 elif action == "BUILD_VERBOSE": 305 build_dir = args[0] 306 if build_dir not in self.build_dirs: 307 self.build_dirs.add(build_dir) 308 message = build_dir 309 update_needed = False 310 else: 311 raise Exception("Unknown build status: %s" % action) 312 313 return BuildOutputResult(None, update_needed, message) 314 315 elif plain_line.startswith("Timing report saved to "): 316 cargo_timings = plain_line[len("Timing report saved to ") :] 317 record_cargo_timings(self.resources, cargo_timings) 318 return BuildOutputResult(None, False, None) 319 320 if log_record := read_serialized_record(line): 321 return BuildOutputResult(None, False, log_record) 322 323 warning = None 324 message = line 325 326 try: 327 warning = self._warnings_collector.process_line(line) 328 except Exception: 329 pass 330 331 return BuildOutputResult(warning, False, message) 332 333 def stop_resource_recording(self): 334 if self._resources_started: 335 self.resources.stop() 336 337 self._resources_started = False 338 339 def finish(self): 340 """Record the end of the build.""" 341 self.stop_resource_recording() 342 self.end_time = time.monotonic() 343 self._finder_end_cpu = self._get_finder_cpu_usage() 344 self.elapsed = self.end_time - self.start_time 345 346 self.warnings_database.prune() 347 self.warnings_database.save_to_file(self._warnings_path) 348 349 def record_usage(self): 350 build_resources_profile_path = None 351 try: 352 # When running on automation, we store the resource usage data in 353 # the upload path, alongside, for convenience, a copy of the HTML 354 # viewer. 355 if "MOZ_AUTOMATION" in os.environ and "UPLOAD_PATH" in os.environ: 356 build_resources_profile_path = mozpath.join( 357 os.environ["UPLOAD_PATH"], "profile_build_resources.json" 358 ) 359 else: 360 build_resources_profile_path = self._get_state_filename( 361 "profile_build_resources.json" 362 ) 363 with open( 364 build_resources_profile_path, "w", encoding="utf-8", newline="\n" 365 ) as fh: 366 to_write = json.dumps( 367 self.resources.as_profile(), separators=(",", ":") 368 ) 369 fh.write(to_write) 370 except Exception as e: 371 self.log( 372 logging.WARNING, 373 "build_resources_error", 374 {"msg": str(e)}, 375 "Exception when writing resource usage file: {msg}", 376 ) 377 try: 378 if build_resources_profile_path and os.path.exists( 379 build_resources_profile_path 380 ): 381 os.remove(build_resources_profile_path) 382 except Exception: 383 # In case there's an exception for some reason, ignore it. 384 pass 385 386 def _get_finder_cpu_usage(self): 387 """Obtain the CPU usage of the Finder app on OS X. 388 389 This is used to detect high CPU usage. 390 """ 391 if not sys.platform.startswith("darwin"): 392 return None 393 394 if not psutil: 395 return None 396 397 for proc in psutil.process_iter(): 398 if proc.name != "Finder": 399 continue 400 401 if proc.username != getpass.getuser(): 402 continue 403 404 # Try to isolate system finder as opposed to other "Finder" 405 # processes. 406 if not proc.exe.endswith("CoreServices/Finder.app/Contents/MacOS/Finder"): 407 continue 408 409 return proc.get_cpu_times() 410 411 return None 412 413 def have_high_finder_usage(self): 414 """Determine whether there was high Finder CPU usage during the build. 415 416 Returns True if there was high Finder CPU usage, False if there wasn't, 417 or None if there is nothing to report. 418 """ 419 if not self._finder_start_cpu: 420 return None, None 421 422 # We only measure if the measured range is sufficiently long. 423 if self.elapsed < 15: 424 return None, None 425 426 if not self._finder_end_cpu: 427 return None, None 428 429 start = self._finder_start_cpu 430 end = self._finder_end_cpu 431 432 start_total = start.user + start.system 433 end_total = end.user + end.system 434 435 cpu_seconds = end_total - start_total 436 437 # If Finder used more than 25% of 1 core during the build, report an 438 # error. 439 finder_percent = cpu_seconds / self.elapsed * 100 440 441 return finder_percent > 25, finder_percent 442 443 def have_excessive_swapping(self): 444 """Determine whether there was excessive swapping during the build. 445 446 Returns a tuple of (excessive, swap_in, swap_out). All values are None 447 if no swap information is available. 448 """ 449 if not self.have_resource_usage: 450 return None, None, None 451 452 swap_in = sum(m.swap.sin for m in self.resources.measurements) 453 swap_out = sum(m.swap.sout for m in self.resources.measurements) 454 455 # The threshold of 1024 MB has been arbitrarily chosen. 456 # 457 # Choosing a proper value that is ideal for everyone is hard. We will 458 # likely iterate on the logic until people are generally satisfied. 459 # If a value is too low, the eventual warning produced does not carry 460 # much meaning. If the threshold is too high, people may not see the 461 # warning and the warning will thus be ineffective. 462 excessive = swap_in > 512 * 1048576 or swap_out > 512 * 1048576 463 return excessive, swap_in, swap_out 464 465 @property 466 def have_resource_usage(self): 467 """Whether resource usage is available.""" 468 return self.resources.start_time is not None 469 470 def get_resource_usage(self): 471 """Produce a data structure containing the low-level resource usage information. 472 473 This data structure can e.g. be serialized into JSON and saved for 474 subsequent analysis. 475 476 If no resource usage is available, None is returned. 477 """ 478 if not self.have_resource_usage: 479 return None 480 481 cpu_percent = self.resources.aggregate_cpu_percent(phase=None, per_cpu=False) 482 io = self.resources.aggregate_io(phase=None) 483 484 return dict( 485 cpu_percent=cpu_percent, 486 io=io, 487 ) 488 489 def log_resource_usage(self, usage): 490 """Summarize the resource usage of this build in a log message.""" 491 492 if not usage: 493 return 494 495 params = dict( 496 duration=self.end_time - self.start_time, 497 cpu_percent=usage["cpu_percent"], 498 io_read_bytes=usage["io"].read_bytes, 499 io_write_bytes=usage["io"].write_bytes, 500 ) 501 502 message = ( 503 "Overall system resources - Wall time: {duration:.0f}s; " 504 "CPU: {cpu_percent:.0f}%; " 505 "Read bytes: {io_read_bytes}; Write bytes: {io_write_bytes}; " 506 ) 507 508 if hasattr(usage["io"], "read_time") and hasattr(usage["io"], "write_time"): 509 params.update( 510 io_read_time=usage["io"].read_time, 511 io_write_time=usage["io"].write_time, 512 ) 513 message += "Read time: {io_read_time}; Write time: {io_write_time}" 514 515 self.log(logging.WARNING, "resource_usage", params, message) 516 517 excessive, sin, sout = self.have_excessive_swapping() 518 if excessive is not None and (sin or sout): 519 sin /= 1048576 520 sout /= 1048576 521 self.log( 522 logging.WARNING, 523 "swap_activity", 524 {"sin": sin, "sout": sout}, 525 "Swap in/out (MB): {sin}/{sout}", 526 ) 527 528 def ccache_stats(self, ccache=None): 529 ccache_stats = None 530 531 if ccache is None: 532 ccache = mozfile.which("ccache") 533 if ccache: 534 # With CCache v3.7+ we can use --print-stats 535 has_machine_format = CCacheStats.check_version_3_7_or_newer(ccache) 536 try: 537 output = subprocess.check_output( 538 [ccache, "--print-stats" if has_machine_format else "-s"], 539 universal_newlines=True, 540 ) 541 ccache_stats = CCacheStats(output, has_machine_format) 542 except ValueError as e: 543 self.log(logging.WARNING, "ccache", {"msg": str(e)}, "{msg}") 544 return ccache_stats 545 546 547 class TerminalLoggingHandler(logging.Handler): 548 """Custom logging handler that works with terminal window dressing. 549 550 This class should probably live elsewhere, like the mach core. Consider 551 this a proving ground for its usefulness. 552 """ 553 554 def __init__(self): 555 logging.Handler.__init__(self) 556 557 self.fh = sys.stdout 558 self.footer = None 559 560 def flush(self): 561 self.acquire() 562 563 try: 564 self.fh.flush() 565 finally: 566 self.release() 567 568 def emit(self, record): 569 msg = self.format(record) 570 571 self.acquire() 572 573 try: 574 if self.footer: 575 self.footer.clear() 576 577 self.fh.write(msg) 578 self.fh.write("\n") 579 580 if self.footer: 581 self.footer.draw() 582 583 # If we don't flush, the footer may not get drawn. 584 self.fh.flush() 585 finally: 586 self.release() 587 588 589 class BuildProgressFooter(Footer): 590 """Handles display of a build progress indicator in a terminal. 591 592 When mach builds inside a blessed-supported terminal, it will render 593 progress information collected from a BuildMonitor. This class converts the 594 state of BuildMonitor into terminal output. 595 """ 596 597 def __init__(self, terminal, monitor): 598 Footer.__init__(self, terminal) 599 self.tiers = monitor.tiers.tier_status.items() 600 601 def draw(self): 602 """Draws this footer in the terminal.""" 603 604 if not self.tiers: 605 return 606 607 # The drawn terminal looks something like: 608 # TIER: static export libs tools 609 610 parts = [("bold", "TIER:")] 611 append = parts.append 612 for tier, status in self.tiers: 613 if status is None: 614 append(tier) 615 elif status == "finished": 616 append(("green", tier)) 617 else: 618 append(("underline_yellow", tier)) 619 620 self.write(parts) 621 622 623 class OutputManager(LoggingMixin): 624 """Handles writing job output to a terminal or log.""" 625 626 def __init__(self, log_manager, footer): 627 self.populate_logger() 628 629 self.footer = None 630 terminal = log_manager.terminal 631 632 # TODO convert terminal footer to config file setting. 633 if not terminal: 634 return 635 if os.environ.get("INSIDE_EMACS", None): 636 return 637 638 if os.environ.get("MACH_NO_TERMINAL_FOOTER", None): 639 footer = None 640 641 self.t = terminal 642 self.footer = footer 643 644 self._handler = TerminalLoggingHandler() 645 self._handler.setFormatter(log_manager.terminal_formatter) 646 self._handler.footer = self.footer 647 648 old = log_manager.replace_terminal_handler(self._handler) 649 self._handler.level = old.level 650 651 def __enter__(self): 652 return self 653 654 def __exit__(self, exc_type, exc_value, traceback): 655 if self.footer: 656 self.footer.clear() 657 # Prevents the footer from being redrawn if logging occurs. 658 self._handler.footer = None 659 660 def write_line(self, line): 661 if self.footer: 662 self.footer.clear() 663 664 print(line) 665 666 if self.footer: 667 self.footer.draw() 668 669 def refresh(self): 670 if not self.footer: 671 return 672 673 self.footer.clear() 674 self.footer.draw() 675 676 677 class BuildOutputManager(OutputManager): 678 """Handles writing build output to a terminal, to logs, etc.""" 679 680 def __init__(self, log_manager, monitor, footer): 681 self.monitor = monitor 682 OutputManager.__init__(self, log_manager, footer) 683 684 def __exit__(self, exc_type, exc_value, traceback): 685 OutputManager.__exit__(self, exc_type, exc_value, traceback) 686 687 # Ensure the resource monitor is stopped because leaving it running 688 # could result in the process hanging on exit because the resource 689 # collection child process hasn't been told to stop. 690 self.monitor.stop_resource_recording() 691 692 def on_line(self, line): 693 warning, state_changed, message = self.monitor.on_line(line) 694 695 if message: 696 if isinstance(message, logging.LogRecord): 697 self.log_record("build_output", message) 698 else: 699 self.log(logging.INFO, "build_output", {"line": message}, "{line}") 700 elif state_changed: 701 have_handler = hasattr(self, "_handler") 702 if have_handler: 703 self._handler.acquire() 704 try: 705 self.refresh() 706 finally: 707 if have_handler: 708 self._handler.release() 709 710 711 class StaticAnalysisFooter(Footer): 712 """Handles display of a static analysis progress indicator in a terminal.""" 713 714 def __init__(self, terminal, monitor): 715 Footer.__init__(self, terminal) 716 self.monitor = monitor 717 718 def draw(self): 719 """Draws this footer in the terminal.""" 720 721 monitor = self.monitor 722 total = monitor.num_files 723 processed = monitor.num_files_processed 724 percent = "(%.2f%%)" % (processed * 100.0 / total) 725 parts = [ 726 ("bright_black", "Processing"), 727 ("yellow", str(processed)), 728 ("bright_black", "of"), 729 ("yellow", str(total)), 730 ("bright_black", "files"), 731 ("green", percent), 732 ] 733 if monitor.current_file: 734 parts.append(("bold", monitor.current_file)) 735 736 self.write(parts) 737 738 739 class StaticAnalysisOutputManager(OutputManager): 740 """Handles writing static analysis output to a terminal or file.""" 741 742 def __init__(self, log_manager, monitor, footer): 743 self.monitor = monitor 744 self.raw = "" 745 OutputManager.__init__(self, log_manager, footer) 746 747 def on_line(self, line): 748 warning, relevant = self.monitor.on_line(line) 749 if relevant: 750 self.raw += line + "\n" 751 752 if warning: 753 self.log( 754 logging.INFO, 755 "compiler_warning", 756 warning, 757 "Warning: {flag} in {filename}: {message}", 758 ) 759 760 if relevant: 761 self.log(logging.INFO, "build_output", {"line": line}, "{line}") 762 else: 763 have_handler = hasattr(self, "_handler") 764 if have_handler: 765 self._handler.acquire() 766 try: 767 self.refresh() 768 finally: 769 if have_handler: 770 self._handler.release() 771 772 def write(self, path, output_format): 773 assert output_format in ( 774 "text", 775 "json", 776 ), f"Invalid output format {output_format}" 777 path = mozpath.realpath(path) 778 779 if output_format == "json": 780 self.monitor._warnings_database.save_to_file(path) 781 782 else: 783 with open(path, "w", encoding="utf-8", newline="\n") as f: 784 f.write(self.raw) 785 786 self.log( 787 logging.INFO, 788 "write_output", 789 {"path": path, "format": output_format}, 790 "Wrote {format} output in {path}", 791 ) 792 793 794 class CCacheStats: 795 """Holds statistics from ccache. 796 797 Instances can be subtracted from each other to obtain differences. 798 print() or str() the object to show a ``ccache -s`` like output 799 of the captured stats. 800 801 """ 802 803 STATS_KEYS = [ 804 # (key, description) 805 # Refer to stats.c in ccache project for all the descriptions. 806 ("stats_zeroed", ("stats zeroed", "stats zero time")), 807 ("stats_updated", "stats updated"), 808 ("cache_hit_direct", "cache hit (direct)"), 809 ("cache_hit_preprocessed", "cache hit (preprocessed)"), 810 ("cache_hit_rate", "cache hit rate"), 811 ("cache_miss", "cache miss"), 812 ("link", "called for link"), 813 ("preprocessing", "called for preprocessing"), 814 ("multiple", "multiple source files"), 815 ("stdout", "compiler produced stdout"), 816 ("no_output", "compiler produced no output"), 817 ("empty_output", "compiler produced empty output"), 818 ("failed", "compile failed"), 819 ("error", "ccache internal error"), 820 ("preprocessor_error", "preprocessor error"), 821 ("cant_use_pch", "can't use precompiled header"), 822 ("compiler_missing", "couldn't find the compiler"), 823 ("cache_file_missing", "cache file missing"), 824 ("bad_args", "bad compiler arguments"), 825 ("unsupported_lang", "unsupported source language"), 826 ("compiler_check_failed", "compiler check failed"), 827 ("autoconf", "autoconf compile/link"), 828 ("unsupported_code_directive", "unsupported code directive"), 829 ("unsupported_compiler_option", "unsupported compiler option"), 830 ("out_stdout", "output to stdout"), 831 ("out_device", "output to a non-regular file"), 832 ("no_input", "no input file"), 833 ("bad_extra_file", "error hashing extra file"), 834 ("num_cleanups", "cleanups performed"), 835 ("cache_files", "files in cache"), 836 ("cache_size", "cache size"), 837 ("cache_max_size", "max cache size"), 838 ] 839 840 SKIP_LINES = ( 841 "cache directory", 842 "primary config", 843 "secondary config", 844 ) 845 846 STATS_KEYS_3_7_PLUS = { 847 "stats_zeroed_timestamp": "stats_zeroed", 848 "stats_updated_timestamp": "stats_updated", 849 "direct_cache_hit": "cache_hit_direct", 850 "preprocessed_cache_hit": "cache_hit_preprocessed", 851 # "cache_hit_rate" is not provided 852 "cache_miss": "cache_miss", 853 "called_for_link": "link", 854 "called_for_preprocessing": "preprocessing", 855 "multiple_source_files": "multiple", 856 "compiler_produced_stdout": "stdout", 857 "compiler_produced_no_output": "no_output", 858 "compiler_produced_empty_output": "empty_output", 859 "compile_failed": "failed", 860 "internal_error": "error", 861 "preprocessor_error": "preprocessor_error", 862 "could_not_use_precompiled_header": "cant_use_pch", 863 "could_not_find_compiler": "compiler_missing", 864 "missing_cache_file": "cache_file_missing", 865 "bad_compiler_arguments": "bad_args", 866 "unsupported_source_language": "unsupported_lang", 867 "compiler_check_failed": "compiler_check_failed", 868 "autoconf_test": "autoconf", 869 "unsupported_code_directive": "unsupported_code_directive", 870 "unsupported_compiler_option": "unsupported_compiler_option", 871 "output_to_stdout": "out_stdout", 872 "output_to_a_non_file": "out_device", 873 "no_input_file": "no_input", 874 "error_hashing_extra_file": "bad_extra_file", 875 "cleanups_performed": "num_cleanups", 876 "files_in_cache": "cache_files", 877 "cache_size_kibibyte": "cache_size", 878 # "cache_max_size" is obsolete and not printed anymore 879 } 880 881 ABSOLUTE_KEYS = {"cache_files", "cache_size", "cache_max_size"} 882 FORMAT_KEYS = {"cache_size", "cache_max_size"} 883 884 GiB = 1024**3 885 MiB = 1024**2 886 KiB = 1024 887 888 def __init__(self, output=None, has_machine_format=False): 889 """Construct an instance from the output of ccache -s.""" 890 self._values = {} 891 892 if not output: 893 return 894 895 if has_machine_format: 896 self._parse_machine_format(output) 897 else: 898 self._parse_human_format(output) 899 900 def _parse_machine_format(self, output): 901 for line in output.splitlines(): 902 line = line.strip() 903 key, _, value = line.partition("\t") 904 stat_key = self.STATS_KEYS_3_7_PLUS.get(key) 905 if stat_key: 906 value = int(value) 907 if key.endswith("_kibibyte"): 908 value *= 1024 909 self._values[stat_key] = value 910 911 (direct, preprocessed, miss) = self.hit_rates() 912 self._values["cache_hit_rate"] = (direct + preprocessed) * 100 913 914 def _parse_human_format(self, output): 915 for line in output.splitlines(): 916 line = line.strip() 917 if line: 918 self._parse_line(line) 919 920 def _parse_line(self, line): 921 for stat_key, stat_description in self.STATS_KEYS: 922 if line.startswith(stat_description): 923 raw_value = self._strip_prefix(line, stat_description) 924 self._values[stat_key] = self._parse_value(raw_value) 925 break 926 else: 927 if not line.startswith(self.SKIP_LINES): 928 raise ValueError("Failed to parse ccache stats output: %s" % line) 929 930 @staticmethod 931 def _strip_prefix(line, prefix): 932 if isinstance(prefix, tuple): 933 for p in prefix: 934 line = CCacheStats._strip_prefix(line, p) 935 return line 936 return line[len(prefix) :].strip() if line.startswith(prefix) else line 937 938 @staticmethod 939 def _parse_value(raw_value): 940 try: 941 # ccache calls strftime with '%c' (src/stats.c) 942 ts = time.strptime(raw_value, "%c") 943 return int(time.mktime(ts)) 944 except ValueError: 945 if raw_value == "never": 946 return 0 947 pass 948 949 value = raw_value.split() 950 unit = "" 951 if len(value) == 1: 952 numeric = value[0] 953 elif len(value) == 2: 954 numeric, unit = value 955 else: 956 raise ValueError("Failed to parse ccache stats value: %s" % raw_value) 957 958 if "." in numeric: 959 numeric = float(numeric) 960 else: 961 numeric = int(numeric) 962 963 if unit in ("GB", "Gbytes"): 964 unit = CCacheStats.GiB 965 elif unit in ("MB", "Mbytes"): 966 unit = CCacheStats.MiB 967 elif unit in ("KB", "Kbytes"): 968 unit = CCacheStats.KiB 969 else: 970 unit = 1 971 972 return int(numeric * unit) 973 974 def hit_rate_message(self): 975 return ( 976 "ccache (direct) hit rate: {:.1%}; (preprocessed) hit rate: {:.1%};" 977 " miss rate: {:.1%}".format(*self.hit_rates()) 978 ) 979 980 def hit_rates(self): 981 direct = self._values["cache_hit_direct"] 982 preprocessed = self._values["cache_hit_preprocessed"] 983 miss = self._values["cache_miss"] 984 total = float(direct + preprocessed + miss) 985 986 if total > 0: 987 direct /= total 988 preprocessed /= total 989 miss /= total 990 991 return (direct, preprocessed, miss) 992 993 def __sub__(self, other): 994 result = CCacheStats() 995 996 for k, prefix in self.STATS_KEYS: 997 if k not in self._values and k not in other._values: 998 continue 999 1000 our_value = self._values.get(k, 0) 1001 other_value = other._values.get(k, 0) 1002 1003 if k in self.ABSOLUTE_KEYS: 1004 result._values[k] = our_value 1005 else: 1006 result._values[k] = our_value - other_value 1007 1008 return result 1009 1010 def __str__(self): 1011 LEFT_ALIGN = 34 1012 lines = [] 1013 1014 for stat_key, stat_description in self.STATS_KEYS: 1015 if stat_key not in self._values: 1016 continue 1017 1018 value = self._values[stat_key] 1019 1020 if stat_key in self.FORMAT_KEYS: 1021 value = "%15s" % self._format_value(value) 1022 else: 1023 value = "%8u" % value 1024 1025 if isinstance(stat_description, tuple): 1026 stat_description = stat_description[0] 1027 1028 lines.append("%s%s" % (stat_description.ljust(LEFT_ALIGN), value)) 1029 1030 return "\n".join(lines) 1031 1032 def __nonzero__(self): 1033 relative_values = [ 1034 v for k, v in self._values.items() if k not in self.ABSOLUTE_KEYS 1035 ] 1036 return all(v >= 0 for v in relative_values) and any( 1037 v > 0 for v in relative_values 1038 ) 1039 1040 def __bool__(self): 1041 return self.__nonzero__() 1042 1043 @staticmethod 1044 def _format_value(v): 1045 if v > CCacheStats.GiB: 1046 return "%.1f Gbytes" % (float(v) / CCacheStats.GiB) 1047 elif v > CCacheStats.MiB: 1048 return "%.1f Mbytes" % (float(v) / CCacheStats.MiB) 1049 else: 1050 return "%.1f Kbytes" % (float(v) / CCacheStats.KiB) 1051 1052 @staticmethod 1053 def check_version_3_7_or_newer(ccache): 1054 output_version = subprocess.check_output( 1055 [ccache, "--version"], universal_newlines=True 1056 ) 1057 return CCacheStats._is_version_3_7_or_newer(output_version) 1058 1059 @staticmethod 1060 def _is_version_3_7_or_newer(output): 1061 if "ccache version" not in output: 1062 return False 1063 1064 major = 0 1065 minor = 0 1066 1067 for line in output.splitlines(): 1068 version = re.search(r"ccache version (\d+).(\d+).*", line) 1069 if version: 1070 major = int(version.group(1)) 1071 minor = int(version.group(2)) 1072 break 1073 1074 return ((major << 8) + minor) >= ((3 << 8) + 7) 1075 1076 1077 class BuildDriver(MozbuildObject): 1078 """Provides a high-level API for build actions.""" 1079 1080 def __init__(self, *args, **kwargs): 1081 MozbuildObject.__init__(self, *args, virtualenv_name="build", **kwargs) 1082 self.metrics = None 1083 self.mach_context = None 1084 1085 def build( 1086 self, 1087 metrics, 1088 what=None, 1089 jobs=0, 1090 job_size=0, 1091 directory=None, 1092 verbose=False, 1093 keep_going=False, 1094 mach_context=None, 1095 append_env=None, 1096 ): 1097 warnings_path = self._get_state_filename("warnings.json") 1098 monitor = self._spawn(BuildMonitor) 1099 monitor.init(warnings_path, self.log_manager.terminal, metrics) 1100 status = self._build( 1101 monitor, 1102 metrics, 1103 what, 1104 jobs, 1105 job_size, 1106 directory, 1107 verbose, 1108 keep_going, 1109 mach_context, 1110 append_env, 1111 ) 1112 1113 record_usage = True 1114 1115 # On automation, only record usage for plain `mach build` 1116 if "MOZ_AUTOMATION" in os.environ and what: 1117 record_usage = False 1118 1119 if record_usage: 1120 monitor.record_usage() 1121 1122 return status 1123 1124 def _build( 1125 self, 1126 monitor, 1127 metrics, 1128 what=None, 1129 jobs=0, 1130 job_size=0, 1131 directory=None, 1132 verbose=False, 1133 keep_going=False, 1134 mach_context=None, 1135 append_env=None, 1136 ): 1137 """Invoke the build backend. 1138 1139 ``what`` defines the thing to build. If not defined, the default 1140 target is used. 1141 """ 1142 self.metrics = metrics 1143 self.mach_context = mach_context 1144 footer = BuildProgressFooter(self.log_manager.terminal, monitor) 1145 1146 # Disable indexing in objdir because it is not necessary and can slow 1147 # down builds. 1148 mkdir(self.topobjdir, not_indexed=True) 1149 1150 with BuildOutputManager(self.log_manager, monitor, footer) as output: 1151 monitor.start() 1152 1153 if directory is not None and not what: 1154 print("Can only use -C/--directory with an explicit target name.") 1155 return 1 1156 1157 if directory is not None: 1158 directory = mozpath.normsep(directory) 1159 if directory.startswith("/"): 1160 directory = directory[1:] 1161 1162 monitor.start_resource_recording() 1163 1164 if self._check_clobber(self.mozconfig, os.environ): 1165 return 1 1166 1167 self.mach_context.command_attrs["clobber"] = False 1168 self.metrics.mozbuild.clobber.set(False) 1169 config = None 1170 try: 1171 config = self.config_environment 1172 except Exception: 1173 # If we don't already have a config environment this is either 1174 # a fresh objdir or $OBJDIR/config.status has been removed for 1175 # some reason, which indicates a clobber of sorts. 1176 self.mach_context.command_attrs["clobber"] = True 1177 self.metrics.mozbuild.clobber.set(True) 1178 1179 # Record whether a clobber was requested so we can print 1180 # a special message later if the build fails. 1181 clobber_requested = False 1182 1183 # Write out any changes to the current mozconfig in case 1184 # they should invalidate configure. 1185 self._write_mozconfig_json() 1186 1187 previous_backend = None 1188 if config is not None: 1189 previous_backend = config.substs.get("BUILD_BACKENDS", [None])[0] 1190 1191 config_rc = None 1192 # Even if we have a config object, it may be out of date 1193 # if something that influences its result has changed. 1194 if config is None or self.build_out_of_date( 1195 mozpath.join(self.topobjdir, "config.status"), 1196 mozpath.join(self.topobjdir, "config_status_deps.in"), 1197 ): 1198 if previous_backend and "Make" not in previous_backend: 1199 clobber_requested = self._clobber_configure() 1200 1201 if config is None: 1202 print(" Config object not found by mach.") 1203 1204 config_rc = self.configure( 1205 metrics, 1206 buildstatus_messages=True, 1207 line_handler=output.on_line, 1208 append_env=append_env, 1209 ) 1210 1211 if config_rc != 0: 1212 return config_rc 1213 1214 config = self.reload_config_environment() 1215 1216 if config.substs.get("MOZ_USING_CCACHE"): 1217 ccache = config.substs.get("CCACHE") 1218 ccache_start = monitor.ccache_stats(ccache) 1219 else: 1220 ccache_start = None 1221 1222 # Collect glean metrics 1223 substs = config.substs 1224 mozbuild_metrics = metrics.mozbuild 1225 mozbuild_metrics.compiler.set(substs.get("CC_TYPE", None)) 1226 1227 def get_substs_flag(name): 1228 return bool(substs.get(name, None)) 1229 1230 host = substs.get("host") 1231 monitor.resources.metadata["oscpu"] = host 1232 target = substs.get("target") 1233 if host != target: 1234 monitor.resources.metadata["abi"] = target 1235 1236 product_name = substs.get("MOZ_BUILD_APP") 1237 app_displayname = substs.get("MOZ_APP_DISPLAYNAME") 1238 if app_displayname: 1239 product_name = app_displayname 1240 app_version = substs.get("MOZ_APP_VERSION") 1241 if app_version: 1242 product_name += " " + app_version 1243 monitor.resources.metadata["product"] = product_name 1244 1245 mozbuild_metrics.artifact.set(get_substs_flag("MOZ_ARTIFACT_BUILDS")) 1246 mozbuild_metrics.debug.set(get_substs_flag("MOZ_DEBUG")) 1247 mozbuild_metrics.opt.set(get_substs_flag("MOZ_OPTIMIZE")) 1248 mozbuild_metrics.ccache.set(get_substs_flag("CCACHE")) 1249 using_sccache = get_substs_flag("MOZ_USING_SCCACHE") 1250 mozbuild_metrics.sccache.set(using_sccache) 1251 mozbuild_metrics.icecream.set(get_substs_flag("CXX_IS_ICECREAM")) 1252 mozbuild_metrics.project.set(substs.get("MOZ_BUILD_APP", "")) 1253 mozbuild_metrics.target.set(target) 1254 1255 all_backends = config.substs.get("BUILD_BACKENDS", [None]) 1256 active_backend = all_backends[0] 1257 1258 status = None 1259 1260 if not config_rc and any([ 1261 self.backend_out_of_date( 1262 mozpath.join(self.topobjdir, "backend.%sBackend" % backend) 1263 ) 1264 for backend in all_backends 1265 ]): 1266 print("Build configuration changed. Regenerating backend.") 1267 args = [ 1268 config.substs["PYTHON3"], 1269 mozpath.join(self.topobjdir, "config.status"), 1270 ] 1271 self.run_process(args, cwd=self.topobjdir, pass_thru=True) 1272 1273 if jobs == 0: 1274 for param in self.mozconfig.get("make_extra") or []: 1275 key, value = param.split("=", 1) 1276 if key == "MOZ_PARALLEL_BUILD": 1277 jobs = int(value) 1278 1279 if "Make" not in active_backend: 1280 backend_cls = get_backend_class(active_backend)(config) 1281 status = backend_cls.build(self, output, jobs, verbose, what) 1282 1283 if status and clobber_requested: 1284 for line in CLOBBER_REQUESTED_MESSAGE.splitlines(): 1285 self.log( 1286 logging.WARNING, "clobber", {"msg": line.rstrip()}, "{msg}" 1287 ) 1288 1289 if what and status is None: 1290 # Collect target pairs. 1291 target_pairs = [] 1292 for target in what: 1293 path_arg = self._wrap_path_argument(target) 1294 1295 if directory is not None: 1296 make_dir = mozpath.join(self.topobjdir, directory) 1297 make_target = target 1298 else: 1299 make_dir, make_target = resolve_target_to_make( 1300 self.topobjdir, path_arg.relpath() 1301 ) 1302 1303 if make_dir is None and make_target is None: 1304 return 1 1305 1306 if config.is_artifact_build and target.startswith("installers-"): 1307 # See https://bugzilla.mozilla.org/show_bug.cgi?id=1387485 1308 print( 1309 "Localized Builds are not supported with Artifact Builds enabled.\n" 1310 "You should disable Artifact Builds (Use --disable-compile-environment " 1311 "in your mozconfig instead) then re-build to proceed." 1312 ) 1313 return 1 1314 1315 # See bug 886162 - we don't want to "accidentally" build 1316 # the entire tree (if that's really the intent, it's 1317 # unlikely they would have specified a directory.) 1318 if not make_dir and not make_target: 1319 print( 1320 "The specified directory doesn't contain a " 1321 "Makefile and the first parent with one is the " 1322 "root of the tree. Please specify a directory " 1323 "with a Makefile or run |mach build| if you " 1324 "want to build the entire tree." 1325 ) 1326 return 1 1327 1328 target_pairs.append((make_dir, make_target)) 1329 1330 # Build target pairs. 1331 for make_dir, make_target in target_pairs: 1332 # We don't display build status messages during partial 1333 # tree builds because they aren't reliable there. This 1334 # could potentially be fixed if the build monitor were more 1335 # intelligent about encountering undefined state. 1336 no_build_status = "1" if make_dir is not None else "" 1337 tgt_env = dict(append_env or {}) 1338 tgt_env["NO_BUILDSTATUS_MESSAGES"] = no_build_status 1339 status = self._run_make( 1340 directory=make_dir, 1341 target=make_target, 1342 line_handler=output.on_line, 1343 log=False, 1344 print_directory=False, 1345 ensure_exit_code=False, 1346 num_jobs=jobs, 1347 job_size=job_size, 1348 silent=not verbose, 1349 append_env=tgt_env, 1350 keep_going=keep_going, 1351 ) 1352 1353 if status != 0: 1354 break 1355 1356 elif status is None: 1357 # If the backend doesn't specify a build() method, then just 1358 # call client.mk directly. 1359 status = self._run_client_mk( 1360 line_handler=output.on_line, 1361 jobs=jobs, 1362 job_size=job_size, 1363 verbose=verbose, 1364 keep_going=keep_going, 1365 append_env=append_env, 1366 ) 1367 1368 self.log( 1369 logging.WARNING, 1370 "warning_summary", 1371 {"count": len(monitor.warnings_database)}, 1372 "{count} compiler warnings present.", 1373 ) 1374 1375 # Try to run the active build backend's post-build step, if possible. 1376 try: 1377 active_backend = config.substs.get("BUILD_BACKENDS", [None])[0] 1378 if active_backend: 1379 backend_cls = get_backend_class(active_backend)(config) 1380 new_status = backend_cls.post_build( 1381 self, output, jobs, verbose, status 1382 ) 1383 status = new_status 1384 except Exception as ex: 1385 self.log( 1386 logging.DEBUG, 1387 "post_build", 1388 {"ex": str(ex)}, 1389 "Unable to run active build backend's post-build step; " 1390 + "failing the build due to exception: {ex}.", 1391 ) 1392 if not status: 1393 # If the underlying build provided a failing status, pass 1394 # it through; otherwise, fail. 1395 status = 1 1396 1397 monitor.finish() 1398 1399 if status == 0: 1400 usage = monitor.get_resource_usage() 1401 if usage: 1402 self.mach_context.command_attrs["usage"] = usage 1403 monitor.log_resource_usage(usage) 1404 1405 # Print the collected compiler warnings. This is redundant with 1406 # inline output from the compiler itself. However, unlike inline 1407 # output, this list is sorted and grouped by file, making it 1408 # easier to triage output. 1409 # 1410 # Only do this if we had a successful build. If the build failed, 1411 # there are more important things in the log to look for than 1412 # whatever code we warned about. 1413 if not status: 1414 # Suppress warnings for 3rd party projects in local builds 1415 # until we suppress them for real. 1416 # TODO remove entries/feature once we stop generating warnings 1417 # in these directories. 1418 pathToThirdparty = mozpath.join( 1419 self.topsrcdir, "tools", "rewriting", "ThirdPartyPaths.txt" 1420 ) 1421 1422 pathToGenerated = mozpath.join( 1423 self.topsrcdir, "tools", "rewriting", "Generated.txt" 1424 ) 1425 1426 if os.path.exists(pathToThirdparty): 1427 with open(pathToThirdparty, encoding="utf-8", newline="\n") as f, open( 1428 pathToGenerated, encoding="utf-8", newline="\n" 1429 ) as g: 1430 # Normalize the path (no trailing /) 1431 LOCAL_SUPPRESS_DIRS = tuple( 1432 [line.strip("\n/") for line in f] 1433 + [line.strip("\n/") for line in g] 1434 ) 1435 else: 1436 # For application based on gecko like thunderbird 1437 LOCAL_SUPPRESS_DIRS = () 1438 1439 suppressed_by_dir = Counter() 1440 1441 THIRD_PARTY_CODE = "third-party code" 1442 suppressed = set( 1443 w.replace("-Wno-error=", "-W") 1444 for w in substs.get("WARNINGS_CFLAGS", []) 1445 + substs.get("WARNINGS_CXXFLAGS", []) 1446 if w.startswith("-Wno-error=") 1447 ) 1448 warnings = [] 1449 for warning in sorted(monitor.instance_warnings): 1450 path = mozpath.normsep(warning["filename"]) 1451 if path.startswith(self.topsrcdir): 1452 path = path[len(self.topsrcdir) + 1 :] 1453 1454 warning["normpath"] = path 1455 1456 if "MOZ_AUTOMATION" not in os.environ: 1457 if path.startswith(LOCAL_SUPPRESS_DIRS): 1458 suppressed_by_dir[THIRD_PARTY_CODE] += 1 1459 continue 1460 1461 if warning["flag"] in suppressed: 1462 suppressed_by_dir[mozpath.dirname(path)] += 1 1463 continue 1464 1465 warnings.append(warning) 1466 1467 if THIRD_PARTY_CODE in suppressed_by_dir: 1468 suppressed_third_party_code = [ 1469 (THIRD_PARTY_CODE, suppressed_by_dir.pop(THIRD_PARTY_CODE)) 1470 ] 1471 else: 1472 suppressed_third_party_code = [] 1473 for d, count in suppressed_third_party_code + sorted( 1474 suppressed_by_dir.items() 1475 ): 1476 self.log( 1477 logging.WARNING, 1478 "suppressed_warning", 1479 {"dir": d, "count": count}, 1480 "(suppressed {count} warnings in {dir})", 1481 ) 1482 1483 for warning in warnings: 1484 if warning["column"] is not None: 1485 self.log( 1486 logging.WARNING, 1487 "compiler_warning", 1488 warning, 1489 "warning: {normpath}:{line}:{column} [{flag}] {message}", 1490 ) 1491 else: 1492 self.log( 1493 logging.WARNING, 1494 "compiler_warning", 1495 warning, 1496 "warning: {normpath}:{line} [{flag}] {message}", 1497 ) 1498 1499 high_finder, finder_percent = monitor.have_high_finder_usage() 1500 if high_finder: 1501 print(FINDER_SLOW_MESSAGE % finder_percent) 1502 1503 if config.substs.get("MOZ_USING_CCACHE"): 1504 ccache_end = monitor.ccache_stats(ccache) 1505 else: 1506 ccache_end = None 1507 1508 ccache_diff = None 1509 if ccache_start and ccache_end: 1510 ccache_diff = ccache_end - ccache_start 1511 if ccache_diff: 1512 self.log( 1513 logging.INFO, 1514 "ccache", 1515 {"msg": ccache_diff.hit_rate_message()}, 1516 "{msg}", 1517 ) 1518 1519 notify_minimum_time = 300 1520 try: 1521 notify_minimum_time = int(os.environ.get("MACH_NOTIFY_MINTIME", "300")) 1522 except ValueError: 1523 # Just stick with the default 1524 pass 1525 1526 if monitor.elapsed > notify_minimum_time: 1527 # Display a notification when the build completes. 1528 self.notify("Build complete" if not status else "Build failed") 1529 1530 if status: 1531 if what and any([ 1532 target for target in what if target not in ("faster", "binaries") 1533 ]): 1534 print( 1535 "Hey! Builds initiated with `mach build " 1536 "$A_SPECIFIC_TARGET` may not always work, even if the " 1537 "code being built is correct. Consider doing a bare " 1538 "`mach build` instead." 1539 ) 1540 return status 1541 1542 if monitor.have_resource_usage: 1543 excessive, swap_in, swap_out = monitor.have_excessive_swapping() 1544 # if excessive: 1545 # print(EXCESSIVE_SWAP_MESSAGE) 1546 1547 print("To view a profile of the build, run |mach resource-usage|.") 1548 1549 long_build = monitor.elapsed > 1200 1550 1551 if long_build: 1552 output.on_line( 1553 "We know it took a while, but your build finally finished successfully!" 1554 ) 1555 if not using_sccache: 1556 output.on_line( 1557 "If you are building Firefox often, SCCache can save you a lot " 1558 "of time. You can learn more here: " 1559 "https://firefox-source-docs.mozilla.org/setup/" 1560 "configuring_build_options.html#sccache" 1561 ) 1562 else: 1563 output.on_line("Your build was successful!") 1564 1565 # Only for full builds because incremental builders likely don't 1566 # need to be burdened with this. 1567 if not what: 1568 try: 1569 # Fennec doesn't have useful output from just building. We should 1570 # arguably make the build action useful for Fennec. Another day... 1571 if self.substs["MOZ_BUILD_APP"] != "mobile/android": 1572 print("To take your build for a test drive, run: |mach run|") 1573 app = self.substs["MOZ_BUILD_APP"] 1574 if app in ("browser", "mobile/android"): 1575 print( 1576 "For more information on what to do now, see " 1577 "https://firefox-source-docs.mozilla.org/setup/contributing_code.html" # noqa 1578 ) 1579 except Exception: 1580 # Ignore Exceptions in case we can't find config.status (such 1581 # as when doing OSX Universal builds) 1582 pass 1583 1584 return status 1585 1586 def configure( 1587 self, 1588 metrics, 1589 options=None, 1590 buildstatus_messages=False, 1591 line_handler=None, 1592 append_env=None, 1593 ): 1594 # Disable indexing in objdir because it is not necessary and can slow 1595 # down builds. 1596 self.metrics = metrics 1597 mkdir(self.topobjdir, not_indexed=True) 1598 self._write_mozconfig_json() 1599 1600 def on_line(line): 1601 self.log(logging.INFO, "build_output", {"line": line}, "{line}") 1602 1603 line_handler = line_handler or on_line 1604 1605 append_env = dict(append_env or {}) 1606 1607 # Back when client.mk was used, `mk_add_options "export ..."` lines 1608 # from the mozconfig would spill into the configure environment, so 1609 # add that for backwards compatibility. 1610 for line in self.mozconfig["make_extra"] or []: 1611 if line.startswith("export "): 1612 k, eq, v = line[len("export ") :].partition("=") 1613 if eq == "=": 1614 append_env[k] = v 1615 1616 build_site = CommandSiteManager.from_environment( 1617 self.topsrcdir, 1618 lambda: get_state_dir(specific_to_topsrcdir=True, topsrcdir=self.topsrcdir), 1619 "build", 1620 get_virtualenv_base_dir(self.topsrcdir), 1621 ) 1622 build_site.ensure() 1623 1624 command = [build_site.python_path, mozpath.join(self.topsrcdir, "configure.py")] 1625 if options: 1626 command.extend(options) 1627 1628 if buildstatus_messages: 1629 append_env["MOZ_CONFIGURE_BUILDSTATUS"] = "1" 1630 line_handler("BUILDSTATUS TIERS configure") 1631 line_handler("BUILDSTATUS TIER_START configure") 1632 1633 env = os.environ.copy() 1634 env.update(append_env) 1635 1636 with subprocess.Popen( 1637 command, 1638 cwd=self.topobjdir, 1639 env=env, 1640 stdout=subprocess.PIPE, 1641 stderr=subprocess.STDOUT, 1642 universal_newlines=True, 1643 ) as process: 1644 for line in process.stdout: 1645 line_handler(line.rstrip()) 1646 status = process.wait() 1647 if buildstatus_messages: 1648 line_handler("BUILDSTATUS TIER_FINISH configure") 1649 if status: 1650 print('*** Fix above errors and then restart with "./mach build"') 1651 else: 1652 print("Configure complete!") 1653 print("Be sure to run |mach build| to pick up any changes") 1654 1655 return status 1656 1657 def install_tests(self): 1658 """Install test files.""" 1659 1660 if self.is_clobber_needed(): 1661 print( 1662 INSTALL_TESTS_CLOBBER.format( 1663 clobber_file=mozpath.join(self.topobjdir, "CLOBBER") 1664 ) 1665 ) 1666 sys.exit(1) 1667 1668 install_test_files(mozpath.normpath(self.topsrcdir), self.topobjdir, "_tests") 1669 1670 def _clobber_configure(self): 1671 # This is an optimistic treatment of the CLOBBER file for when we have 1672 # some trust in the build system: an update to the CLOBBER file is 1673 # interpreted to mean that configure will fail during an incremental 1674 # build, which is handled by removing intermediate configure artifacts 1675 # and subsections of the objdir related to python and testing before 1676 # proceeding. 1677 clobberer = Clobberer(self.topsrcdir, self.topobjdir) 1678 clobber_output = io.StringIO() 1679 res = clobberer.maybe_do_clobber(os.getcwd(), False, clobber_output) 1680 required, performed, message = res 1681 assert not performed 1682 if not required: 1683 return False 1684 1685 def remove_objdir_path(path): 1686 path = mozpath.join(self.topobjdir, path) 1687 self.log( 1688 logging.WARNING, 1689 "clobber", 1690 {"path": path}, 1691 "CLOBBER file has been updated, removing {path}.", 1692 ) 1693 mozfile.remove(path) 1694 1695 # Remove files we think could cause "configure" clobber bugs. 1696 for f in ("old-configure.vars", "config.cache", "configure.pkl"): 1697 remove_objdir_path(f) 1698 remove_objdir_path(mozpath.join("js", "src", f)) 1699 1700 rm_dirs = [ 1701 # Stale paths in our virtualenv may cause build-backend 1702 # to fail. 1703 "_virtualenvs", 1704 # Some tests may accumulate state in the objdir that may 1705 # become invalid after srcdir changes. 1706 "_tests", 1707 ] 1708 1709 for d in rm_dirs: 1710 remove_objdir_path(d) 1711 1712 os.utime(mozpath.join(self.topobjdir, "CLOBBER"), None) 1713 return True 1714 1715 def _write_mozconfig_json(self): 1716 mozconfig_json = mozpath.join(self.topobjdir, ".mozconfig.json") 1717 with FileAvoidWrite(mozconfig_json) as fh: 1718 to_write = json.dumps( 1719 { 1720 "topsrcdir": self.topsrcdir, 1721 "topobjdir": self.topobjdir, 1722 "mozconfig": self.mozconfig, 1723 }, 1724 sort_keys=True, 1725 indent=2, 1726 ) 1727 1728 # json.dumps in python2 inserts some trailing whitespace while 1729 # json.dumps in python3 does not, which defeats the FileAvoidWrite 1730 # mechanism. Strip the trailing whitespace to avoid rewriting this 1731 # file unnecessarily. 1732 to_write = "\n".join([line.rstrip() for line in to_write.splitlines()]) 1733 fh.write(to_write) 1734 1735 def _run_client_mk( 1736 self, 1737 target=None, 1738 line_handler=None, 1739 jobs=0, 1740 job_size=0, 1741 verbose=None, 1742 keep_going=False, 1743 append_env=None, 1744 ): 1745 append_env = dict(append_env or {}) 1746 append_env["TOPSRCDIR"] = self.topsrcdir 1747 1748 append_env["CONFIG_GUESS"] = self.resolve_config_guess() 1749 1750 mozconfig = self.mozconfig 1751 1752 mozconfig_make_lines = [] 1753 for arg in mozconfig["make_extra"] or []: 1754 mozconfig_make_lines.append(arg) 1755 1756 if mozconfig["make_flags"]: 1757 mozconfig_make_lines.append( 1758 "MOZ_MAKE_FLAGS=%s" % " ".join(mozconfig["make_flags"]) 1759 ) 1760 objdir = mozpath.normsep(self.topobjdir) 1761 mozconfig_make_lines.append("MOZ_OBJDIR=%s" % objdir) 1762 mozconfig_make_lines.append("OBJDIR=%s" % objdir) 1763 1764 if mozconfig["path"]: 1765 mozconfig_make_lines.append( 1766 "FOUND_MOZCONFIG=%s" % mozpath.normsep(mozconfig["path"]) 1767 ) 1768 mozconfig_make_lines.append("export FOUND_MOZCONFIG") 1769 1770 # The .mozconfig.mk file only contains exported variables and lines with 1771 # UPLOAD_EXTRA_FILES. 1772 mozconfig_filtered_lines = [ 1773 line 1774 for line in mozconfig_make_lines 1775 # Bug 1418122 investigate why UPLOAD_EXTRA_FILES is special and 1776 # remove it. 1777 if line.startswith("export ") or "UPLOAD_EXTRA_FILES" in line 1778 ] 1779 1780 mozconfig_client_mk = mozpath.join(self.topobjdir, ".mozconfig-client-mk") 1781 with FileAvoidWrite(mozconfig_client_mk) as fh: 1782 fh.write("\n".join(mozconfig_make_lines)) 1783 1784 mozconfig_mk = mozpath.join(self.topobjdir, ".mozconfig.mk") 1785 with FileAvoidWrite(mozconfig_mk) as fh: 1786 fh.write("\n".join(mozconfig_filtered_lines)) 1787 1788 # Copy the original mozconfig to the objdir. 1789 mozconfig_objdir = mozpath.join(self.topobjdir, ".mozconfig") 1790 if mozconfig["path"]: 1791 with open(mozconfig["path"]) as ifh: 1792 with FileAvoidWrite(mozconfig_objdir) as ofh: 1793 ofh.write(ifh.read()) 1794 else: 1795 try: 1796 os.unlink(mozconfig_objdir) 1797 except OSError as e: 1798 if e.errno != errno.ENOENT: 1799 raise 1800 1801 if mozconfig_make_lines: 1802 self.log( 1803 logging.WARNING, 1804 "mozconfig_content", 1805 { 1806 "path": mozconfig["path"], 1807 "content": "\n ".join(mozconfig_make_lines), 1808 }, 1809 "Adding make options from {path}\n {content}", 1810 ) 1811 1812 append_env["OBJDIR"] = mozpath.normsep(self.topobjdir) 1813 1814 return self._run_make( 1815 srcdir=True, 1816 filename="client.mk", 1817 ensure_exit_code=False, 1818 print_directory=False, 1819 target=target, 1820 line_handler=line_handler, 1821 log=False, 1822 num_jobs=jobs, 1823 job_size=job_size, 1824 silent=not verbose, 1825 keep_going=keep_going, 1826 append_env=append_env, 1827 ) 1828 1829 def _check_clobber(self, mozconfig, env): 1830 """Run `Clobberer.maybe_do_clobber`, log the result and return a status bool. 1831 1832 Wraps the clobbering logic in `Clobberer.maybe_do_clobber` to provide logging 1833 and handling of the `AUTOCLOBBER` mozconfig option. 1834 1835 Return a bool indicating whether the clobber reached an error state. For example, 1836 return `True` if the clobber was required but not completed, and return `False` if 1837 the clobber was not required and not completed. 1838 """ 1839 auto_clobber = any([ 1840 env.get("AUTOCLOBBER", False), 1841 (mozconfig["env"] or {}).get("added", {}).get("AUTOCLOBBER", False), 1842 "AUTOCLOBBER=1" in (mozconfig["make_extra"] or []), 1843 ]) 1844 from mozbuild.base import BuildEnvironmentNotFoundException 1845 1846 substs = dict() 1847 try: 1848 substs = self.substs 1849 except BuildEnvironmentNotFoundException: 1850 # We'll just use an empty substs if there is no config. 1851 pass 1852 clobberer = Clobberer(self.topsrcdir, self.topobjdir, substs) 1853 clobber_output = io.StringIO() 1854 res = clobberer.maybe_do_clobber(os.getcwd(), auto_clobber, clobber_output) 1855 clobber_output.seek(0) 1856 for line in clobber_output.readlines(): 1857 self.log(logging.WARNING, "clobber", {"msg": line.rstrip()}, "{msg}") 1858 1859 clobber_required, clobber_performed, clobber_message = res 1860 if clobber_required and not clobber_performed: 1861 for line in clobber_message.splitlines(): 1862 self.log(logging.WARNING, "clobber", {"msg": line.rstrip()}, "{msg}") 1863 return True 1864 1865 if clobber_performed and env.get("TINDERBOX_OUTPUT"): 1866 self.log( 1867 logging.WARNING, 1868 "clobber", 1869 {"msg": "TinderboxPrint: auto clobber"}, 1870 "{msg}", 1871 ) 1872 1873 return False