diff -urN portage-3.0.82-orig/lib/portage/const.py portage-3.0.82/lib/portage/const.py --- portage-3.0.82-orig/lib/portage/const.py 2026-08-22 14:02:19 +0900 +++ portage-3.0.82/lib/portage/const.py 2026-08-24 01:16:34 +0900 @@ -180,6 +180,7 @@ "compress-build-logs", "compressdebug", "compress-index", + "concise-progress", "config-protect-if-modified", "dedupdebug", "digest", diff -urN portage-3.0.82-orig/lib/portage/meson.build portage-3.0.82/lib/portage/meson.build --- portage-3.0.82-orig/lib/portage/meson.build 2026-08-22 14:02:19 +0900 +++ portage-3.0.82/lib/portage/meson.build 2026-08-24 01:16:34 +0900 @@ -42,6 +42,7 @@ 'output.py', 'process.py', 'progress.py', + 'timing.py', 'update.py', 'versions.py', 'xpak.py', diff -urN portage-3.0.82-orig/lib/portage/output.py portage-3.0.82/lib/portage/output.py --- portage-3.0.82-orig/lib/portage/output.py 2026-08-22 14:02:19 +0900 +++ portage-3.0.82/lib/portage/output.py 2026-08-24 01:16:51 +0900 @@ -10,6 +10,7 @@ import socket import subprocess import sys +import time import portage from portage.const import COLOR_MAP_FILE @@ -966,3 +967,102 @@ codes = _LazyInitColorMap(codes) _styles = _LazyInitColorMap(_styles) + +class EProgressBar: + """ + Implements a textual progress bar. + + @ivar progress: The current progress through the operation, in either + bytes or arbitrary units. + @type progress: IntType + @ivar total: The maximum value C{progress} is expected to reach, in the + same units as C{progress}. If zero or negative, assumed to be + unknown. + @type total: IntType + @ivar is_bytes: True if C{progress} and C{total} are byte counters, + otherwise false. Affects the manner in which the progress is + displayed. + @type is_bytes: Boolean + @ivar start_time: Time at which the operation was started (assumed to + be the time at which the progress bar was created). + @type start_time: Float + @ivar start_progress: The value of C{progress} when the operation was + started. + @type start_progress: IntType + @ivar term_columns: Width of terminal in characters. Defaults to the value + specified by the shell's C{COLUMNS} variable, else to the queried tty + size, else to C{80}. + @type term_columns: IntType + """ + + def __init__(self, total, progress = 0, is_bytes = False): + self.total = total + self.progress = progress + self.is_bytes = is_bytes + self.start_time = time.time() + self.start_progress = self.progress + lines, columns = get_term_size() + if columns <= 0: + columns = 80 + # Adjust columns so that eend works properly on a standard BSD console. + if os.environ.get("TERM") == "cons25": + columns = columns - 1 + self.term_columns = columns + + def __size_to_string(self, size): + if size >= 100000000000: + return "%uGB" % (size / 1000000000) + elif size >= 100000000: + return "%uMB" % (size / 1000000) + else: + return "%ukB" % (size / 1000) + + def update(self, progress = -1): + """ + Optionally updates the current progress value, then draws the + progress bar on the terminal. + + @param progress: I{(optional)} The current progress value. + @type progress: IntType + """ + if progress >= 0: + self.progress = progress + if self.is_bytes: + progress_string = self.__size_to_string(self.progress) + total_string = self.__size_to_string(self.total) + else: + progress_string = "" + total_string = "%d%% " % (self.progress*100 / self.total) + if self.total > 0: + bar_size = self.term_columns - 27 + time_string = "---:--" + elapsed_time = time.time() - self.start_time + elapsed_progress = self.progress - self.start_progress + progress_left = (self.total - self.start_progress) - elapsed_progress + if elapsed_time > 0 and elapsed_progress > 0 and progress_left >= 0: + import math + eta = int(math.ceil((elapsed_time * progress_left) / elapsed_progress)) + if eta > 1000*60-1: + eta = 1000*60-1 + time_string = "%3d:%02d" % (eta/60, eta%60) + if bar_size > 0: + bar_width = int(bar_size * min(self.progress / self.total, 1)) + bar_string = "%-*s" % (bar_size, '=' * bar_width) + sys.stderr.write("%7s |%s| %7s %s\r" \ + % (progress_string, bar_string, total_string, time_string)) + else: + sys.stderr.write("%7s/%-7s %s\r" \ + % (progress_string, total_string, time_string)) + else: + sys.stderr.write("%7s\r" % progress_string) + sys.stdout.flush() + + def finish(self): + """ + Draws the progress bar on the terminal, then advances to the next + line so that subsequent terminal output does not overwrite the + progress bar. + """ + from portage.util import writemsg + self.update() + writemsg("\n") diff -urN portage-3.0.82-orig/lib/portage/package/ebuild/digestcheck.py portage-3.0.82/lib/portage/package/ebuild/digestcheck.py --- portage-3.0.82-orig/lib/portage/package/ebuild/digestcheck.py 2026-08-22 14:02:19 +0900 +++ portage-3.0.82/lib/portage/package/ebuild/digestcheck.py 2026-08-24 01:16:34 +0900 @@ -42,6 +42,8 @@ ) mf = mf.load_manifest(pkgdir, mysettings["DISTDIR"]) eout = EOutput() + eout.quiet = mysettings.get("PORTAGE_QUIET", None) == "1" \ + or "concise-progress" in mysettings.features eout.quiet = mysettings.get("PORTAGE_QUIET", None) == "1" try: if not mf.thin and strict and "PORTAGE_PARALLEL_FETCHONLY" not in mysettings: diff -urN portage-3.0.82-orig/lib/portage/package/ebuild/fetch.py portage-3.0.82/lib/portage/package/ebuild/fetch.py --- portage-3.0.82-orig/lib/portage/package/ebuild/fetch.py 2026-08-22 14:02:19 +0900 +++ portage-3.0.82/lib/portage/package/ebuild/fetch.py 2026-08-24 01:17:30 +0900 @@ -46,8 +46,9 @@ ) from portage.localization import _ from portage.locks import lockfile, unlockfile -from portage.output import EOutput, colorize +from portage.output import EOutput, EProgressBar, colorize from portage.process import spawn +import portage.timing from portage.util import ( apply_recursive_permissions, apply_secpass_permissions, @@ -173,13 +174,56 @@ _async_spawn_fetch_post_terminate = None -async def _async_spawn_fetch(settings, args, **kwargs): +async def _async_spawn_fetch(settings, args, progress_bar=None, download_path=None, fetched_size=0, **kwargs): kwargs["returnproc"] = True + if progress_bar: + # Hide stdout/err so we don't disturb the progress bar + if "fd_pipes" not in kwargs: # as in _spawn_fetch() + kwargs["fd_pipes"] = { + 0: portage._get_stdin().fileno(), + 1: sys.__stdout__.fileno(), + 2: sys.__stdout__.fileno(), + } + try: + fd = os.open("/dev/null", os.O_RDONLY) + kwargs["fd_pipes"][1] = fd + kwargs["fd_pipes"][2] = fd + except OSError as e: + pass proc = _spawn_fetch(settings, args, **kwargs) try: if _async_spawn_fetch_pre_wait is not None: _async_spawn_fetch_pre_wait(proc) - return await proc.wait() + if progress_bar: + # Wait for the process to complete, updating the + # file size as we go + while proc._proc.is_alive(): + # Update the progress bar with the current file size + try: + filesize = os.stat(download_path).st_size + progress_bar.update(fetched_size + filesize) + except EnvironmentError: + pass + # Wait a bit so we don't sit on the CPU too much + time.sleep(0.1) + # Portage voodoo equivalent to proc._proc.join() + loop = asyncio.get_event_loop() + if loop.is_running(): + asyncio.ensure_future(proc.wait(), loop).add_done_callback( + lambda future: future.result() + ) + else: + loop.run_until_complete(proc.wait()) + rval = proc.returncode + # Do one final update to fill the progress bar + try: + filesize = os.stat(download_path).st_size + progress_bar.update(fetched_size + filesize) + except EnvironmentError: + pass + return rval + else: # not progress_bar + return await proc.wait() except asyncio.CancelledError: proc.terminate() if _async_spawn_fetch_post_terminate is not None: @@ -1221,14 +1265,20 @@ valid_hashes = set(get_valid_checksum_keys()) valid_hashes.discard("size") + concise_progress = "concise-progress" in features + + total_size = 0 # Total size to fetch in bytes, negative = unknown + fetched_size = 0 # Amount fetched so far in bytes + filestatus = {} # Status of each file (see below) + filelocks = {} # Lock for each file for myfile in filedict: """ - fetched status - 0 nonexistent - 1 partially downloaded - 2 completely downloaded + filestatus + 0 = nonexistent + 1 = partially downloaded + 2 = completely downloaded """ - fetched = 0 + filestatus[myfile] = 0 orig_digests = mydigests.get(myfile, {}) @@ -1275,7 +1325,7 @@ download_path = myfile_path if fetch_to_ro else myfile_path + _download_suffix has_space = True has_space_superuser = True - file_lock = None + filelocks[myfile] = None if listonly: writemsg_stdout("\n", noiselevel=-1) else: @@ -1318,7 +1368,8 @@ lock_kwargs["flags"] = os.O_NONBLOCK try: - file_lock = lockfile(myfile_path, wantnewlockfile=1, **lock_kwargs) + filelocks[myfile] = lockfile(myfile_path, + wantnewlockfile=1, **lock_kwargs) except TryAgain: writemsg( _( @@ -1329,9 +1380,12 @@ noiselevel=-1, ) continue - try: - if not listonly: + + if not listonly: # no longer needed, but left in to avoid unindentation creating huge diffs --AC eout = EOutput() + eout.quiet = concise_progress or ( + mysettings.get("PORTAGE_QUIET") == "1" + ) eout.quiet = mysettings.get("PORTAGE_QUIET") == "1" match, mystat = _check_distfile( myfile_path, pruned_digests, eout, hash_filter=hash_filter @@ -1357,6 +1411,7 @@ noiselevel=-1, ) del e + filestatus[myfile] = 2 continue # Remove broken symlinks or symlinks to files which @@ -1471,6 +1526,7 @@ raise del e os.symlink(readonly_file, myfile_path) + filestatus[myfile] = 2 continue # this message is shown only after we know that @@ -1512,6 +1568,11 @@ raise del e + if myfile not in mydigests: + total_size = -1 # At least one file has an unknown size, so give up on counting + elif total_size >= 0: + total_size += mydigests[myfile]["size"] + try: mystat = os.stat(download_path) except OSError as e: @@ -1559,12 +1620,15 @@ and mystat.st_size < mydigests[myfile]["size"] and not restrict_fetch ): - fetched = 1 # Try to resume this download. + filestatus[myfile] = 1 # Try to resume this download. + fetched_size += mystat.st_size elif ( parallel_fetchonly and mystat.st_size == mydigests[myfile]["size"] ): - eout = EOutput() + eout.quiet = concise_progress or ( + mysettings.get("PORTAGE_QUIET") == "1" + ) eout.quiet = mysettings.get("PORTAGE_QUIET") == "1" eout.ebegin(f"{myfile} size ;-)") eout.eend(0) @@ -1611,7 +1675,7 @@ mysettings=mysettings, ) eout = EOutput() - eout.quiet = ( + eout.quiet = concise_progress or ( mysettings.get("PORTAGE_QUIET", None) == "1" ) if digests: @@ -1619,8 +1683,26 @@ digests.sort() eout.ebegin(f"{myfile} {' '.join(digests)} ;-)") eout.eend(0) - continue # fetch any remaining files - + if total_size >= 0: + total_size -= mydigests[myfile]["size"] + filestatus[myfile] = 2 # No need to fetch this one + + progress_bar = None + if concise_progress and not listonly and total_size != 0: + progress_bar = EProgressBar(total_size, fetched_size, is_bytes=1) + for myfile in filedict: + myfile_path = os.path.join(mysettings["DISTDIR"], myfile) + if progress_bar: + # If it's a partially-completed file, the file size will be + # already included in fetched_size. Show the progress bar + # as is, then remove this file's contribution (since we'll + # add it back in later on). + progress_bar.update(fetched_size) + try: + fetched_size -= os.stat(myfile_path).st_size + except EnvironmentError: + pass + try: # Create a reversed list since that is optimal for list.pop(). uri_list = filedict[myfile][:] uri_list.reverse() @@ -1719,7 +1801,9 @@ return 0 if not can_fetch: - if fetched != 2: + if filestatus[myfile] != 2: + if progress_bar: + progress_bar.finish() try: mysize = os.stat(download_path).st_size except OSError as e: @@ -1754,16 +1838,16 @@ return 0 continue - if fetched != 2 and has_space: + if filestatus[myfile] != 2 and has_space: # we either need to resume or start the download - if fetched == 1: + if filestatus[myfile] == 1: try: mystat = os.stat(download_path) except OSError as e: if e.errno not in (errno.ENOENT, errno.ESTALE): raise del e - fetched = 0 + filestatus[myfile] = 0 else: if distdir_writable and mystat.st_size < fetch_resume_size: writemsg( @@ -1781,17 +1865,19 @@ if e.errno not in (errno.ENOENT, errno.ESTALE): raise del e - fetched = 0 - if fetched == 1: + filestatus[myfile] = 0 + if filestatus[myfile] == 1: # resume mode: - writemsg(_(">>> Resuming download...\n")) + if not progress_bar: + writemsg(_(">>> Resuming download...\n")) locfetch = resumecommand command_var = resumecommand_var else: # normal mode: locfetch = fetchcommand command_var = fetchcommand_var - writemsg_stdout(_(">>> Downloading '%s'\n") % _hide_url_passwd(loc)) + if not progress_bar: + writemsg_stdout(_(">>> Downloading '%s'\n") % _hide_url_passwd(loc)) variables = {"URI": loc, "FILE": os.path.basename(download_path)} try: @@ -1823,7 +1909,7 @@ myret = -1 try: - myret = await _async_spawn_fetch(mysettings, myfetch) + myret = await _async_spawn_fetch(mysettings, myfetch, progress_bar, download_path, fetched_size) finally: try: @@ -1834,6 +1920,8 @@ pass except PortageException as e: if not os.access(download_path, os.R_OK): + if progress_bar: + progress_bar.finish() writemsg( _("!!! Failed to adjust permissions:" " %s\n") % str(e), @@ -1851,7 +1939,7 @@ and not os.path.exists(download_path) ): os.unlink(download_path) - fetched = 0 + filestatus[myfile] = 0 continue except OSError: pass @@ -1863,7 +1951,7 @@ if e.errno not in (errno.ENOENT, errno.ESTALE): raise del e - fetched = 0 + filestatus[myfile] = 0 else: if stat.S_ISDIR(mystat.st_mode): # This can happen if FETCHCOMMAND erroneously @@ -1931,11 +2019,11 @@ ">>> Deleting invalid distfile. (Improper 404 redirect from server.)\n" ) ) - fetched = 0 + filestatus[myfile] = 0 continue except OSError: pass - fetched = 1 + filestatus[myfile] = 1 continue if True: # File is the correct size--check the checksums for the fetched @@ -1949,6 +2037,8 @@ digests = _apply_hash_filter(digests, hash_filter) verified_ok, reason = verify_all(download_path, digests) if not verified_ok: + if progress_bar: + progress_bar.finish() writemsg( _("!!! Fetched file: %s VERIFY FAILED!\n") % myfile, @@ -1980,7 +2070,7 @@ % temp_filename, noiselevel=-1, ) - fetched = 0 + filestatus[myfile]=0 checksum_failure_count += 1 if ( checksum_failure_count @@ -2005,7 +2095,7 @@ mysettings=mysettings, ) eout = EOutput() - eout.quiet = ( + eout.quiet = progress_bar or ( mysettings.get("PORTAGE_QUIET", None) == "1" ) if digests: @@ -2013,7 +2103,7 @@ f"{myfile} {' '.join(sorted(digests))} ;-)" ) eout.eend(0) - fetched = 2 + filestatus[myfile]=2 break else: # no digests available if not myret: @@ -2021,7 +2111,7 @@ _movefile( download_path, myfile_path, mysettings=mysettings ) - fetched = 2 + filestatus[myfile]=2 break elif mydigests is not None: writemsg( @@ -2029,13 +2119,19 @@ noiselevel=-1, ) finally: - if use_locks and file_lock: - unlockfile(file_lock) - file_lock = None + if use_locks and filelocks[myfile]: + unlockfile(filelocks[myfile]) + filelocks[myfile] = None if listonly: writemsg_stdout("\n", noiselevel=-1) - if fetched != 2: + if progress_bar: + # Save the final progress from this file + fetched_size = progress_bar.progress; + if filestatus[myfile] != 2: + if progress_bar: + progress_bar.finish() + progress_bar = None if restrict_fetch and not restrict_fetch_msg: restrict_fetch_msg = True msg = _( @@ -2065,6 +2161,10 @@ return 0 if failed_files: return 0 + + if progress_bar: + progress_bar.finish() + return 1 diff -urN portage-3.0.82-orig/lib/portage/timing.py portage-3.0.82/lib/portage/timing.py --- portage-3.0.82-orig/lib/portage/timing.py 1970-01-01 09:00:00 +0900 +++ portage-3.0.82/lib/portage/timing.py 2026-08-24 01:16:34 +0900 @@ -0,0 +1,330 @@ +# portage_timing.py -- routines to time build execution +# Written by Andrew Church +# This source code is public domain. + +import os, re, select, sys, time +import portage +portage.proxy.lazyimport.lazyimport(globals(), + 'portage.util:writemsg', 'portage.util:writemsg_stdout', + 'portage.versions:pkgsplit', 'output', +) + + +# Output recorded from the current build: +timing_output = [] +# Time at which each line in timing_output[] was recorded: +timing_outtimes = [] +# Current position in timing_{output,times}[]: (to avoid having to extend +# the array every time a new element is added) +timing_outpos = 0 + + +def read_timing(timingfile): + """ + Read a possibly compressed timing file, returning the timing template + as an array of 3-element arrays: the first element of each subarray is + the elapsed time in microseconds between that line and the next, the + second element is the line itself as a string (with no trailing + newline), and the third is the completion fraction at the time the line + is first seen. The timing template array is terminated by an entry + whose first (time) element is the total execution time in microseconds, + whose third (fraction) element is 1.0, and whose remaining elements are + undefined. + + @param timingfile Full pathname of the timing file, without any + compression extension. + @type timingfile String + """ + try: + f = open(timingfile, "r") + timing = [] + for line in f: + timestamp, command = line.split(" ", 1) + command = command.strip("\n") + timing.append([int(timestamp), command, 0.0]) + f.close() + total = 0 + for entry in timing: + entry[2] = (total * 1.0) / timing[-1][0] + total += entry[0] + return timing + except EnvironmentError: + # FIXME: compression not handled at present + return None + + +def write_timing(timingfile): + """ + Write a new timing file from the timing_output[] and timing_outtimes[] + arrays. + + @param timingfile Full pathname of the timing file, without any + compression extension. + @type timingfile String + """ + + try: + timing_outtimes.append(None) # just in case + timing_outtimes[timing_outpos] = time.time() + f = open(timingfile, "w") + total = 0 + for i in range(0, timing_outpos): + line_time = int((timing_outtimes[i+1] - timing_outtimes[i]) * 1000000) + f.write("%u %s\n" % (line_time, timing_output[i])) + total += line_time + f.write("%u -\n" % total) + f.close() + # FIXME: compression not handled at present + except EnvironmentError: + pass + + +def timed_spawnebuild(timingfile, mydo, actionmap, mysettings, debug, + alwaysdep=0, logfile=None, fd_pipes=None, returnpid=False): + """ + Call spawnebuild() in a subprocess, comparing the output to a template + file to display a progress bar instead of the raw output (if the + template file is unavailable, the raw output is shown). If the + operation succeeds, the output and timing information is recorded in + the same template file for future reference. + + @param timingfile Full pathname of the timing file, without any + compression extension. + @type timingfile String + + The remaining parameters are as for spawnebuild(). + """ + + # Read in the timing file, if it exists, and initialize the template + # index. + timing = read_timing(timingfile) + timing_index = -1 + + # Allocate a pseudo-TTY for reading output from the subprocess. If we + # can't, just give up and run the build normally. + master_fd = None + slave_fd = None + from pty import openpty + try: + master_fd, slave_fd = openpty() + except EnvironmentError as e: + writemsg("openpty failed: '%s'\n" % str(e), noiselevel=1) + del e + return portage.spawnebuild(mydo, actionmap, mysettings, debug, + alwaysdep, logfile, fd_pipes, returnpid) + + # Run spawnebuild() in a subprocess, with output redirected to our + # pseudo-TTY. + pid = os.fork() + if pid == 0: + os.close(0) + try: + null_fd = os.open("/dev/null", os.O_RDONLY) + if null_fd != 0: + os.dup2(null_fd, 0) + os.close(null_fd) + except EnvironmentError: + pass + os.dup2(slave_fd, 1) + os.dup2(slave_fd, 2) + os.close(master_fd) + os.close(slave_fd) + retval = portage.spawnebuild(mydo, actionmap, mysettings, debug, + alwaysdep, logfile) + sys.exit(retval) + else: + os.close(slave_fd) + import fcntl + fcntl.fcntl(master_fd, fcntl.F_SETFL, + fcntl.fcntl(master_fd, fcntl.F_GETFL) | os.O_NONBLOCK) + + # Set up a progress bar if we have a timing template. + progress_bar = None + if timing: + progress_bar = output.EProgressBar(timing[-1][0]) + progress_bar.update(0) + last_update = time.time() + + # Clear the new timing data. + global timing_output, timing_outtimes, timing_outpos + timing_output = [] + timing_outtimes = [] + timing_outpos = 0 + + # Read output from the subprocess until it completes, updating the new + # timing template for each line read. + master_file = os.fdopen(master_fd, "r") + saved_line = "" # In case we only get a partial line when reading + eof = False # Becomes true when we get an EOF from the pseudo-TTY + finished = False # Becomes true when the subprocess exits + result = None # Result code from the subprocess + while not finished or not eof: + # Check for subprocess completion. + waitres = os.waitpid(pid, os.WNOHANG) + if waitres and waitres[0] == pid: + finished = True + if waitres[1] == os.EX_OK: + result = os.EX_OK + elif waitres[1] & 0xFF: + result = (waitres[1] & 0xFF) << 8 + else: + result = waitres[1] >> 8 + # Check for build output. + events = select.select([master_file], [], [], 0.1) + if events: + s = None + try: + s = master_file.read(1000) + except IOError: + eof = True + if s is None: + if len(saved_line) > 0: + # The final line didn't end with a newline, so add one. + s = "\n" + s = "" + eof = True + elif s == "": + break # No more output this time around + saved_line += s + now = time.time() + index = saved_line.find("\n") + while index >= 0: + line = saved_line[0:index] + saved_line = saved_line[index+1:] + timing_index = add_line(line, timing, timing_index, \ + master_file, mysettings) + if not timing: + writemsg_stdout("%s\n" % line); + index = saved_line.find("\n") + # Update the progress bar (but not too frequently). + if timing and now - last_update >= 0.01: + # Calculate the rate at which we're progressing relative to the + # data recorded in the template. + rate = 0 + if timing_index >= 0 and timing_outpos > 0 \ + and timing[timing_index][2] > 0: + rate = (timing_outtimes[timing_outpos-1] - timing_outtimes[0]) \ + / (timing[-1][0]/1000000.0 * timing[timing_index][2]) + # Calculate the percentage complete, including partial processing + # for the current line. + pct = 0 + if timing_index >= 0: + pct = timing[timing_index][2] + if rate > 0 and now > timing_outtimes[timing_outpos-1]: + extra_time = now - timing_outtimes[timing_outpos-1] + extra_pct = (extra_time / rate) / (timing[-1][0] / 1000000.0) + if extra_pct > timing[timing_index+1][2] - pct: + extra_pct = timing[timing_index+1][2] - pct + pct += extra_pct + # Update the progress bar, making sure it uses our idea of the + # starting time. + if timing_outpos > 0: + progress_bar.start_time = timing_outtimes[0] + progress_bar.update(int(timing[-1][0] * pct)) + # Save the last update time to avoid over-frequent updates. + last_update = now + # while not finished + + # Finish off the progress bar. + if timing: + progress_bar.progress = progress_bar.total + progress_bar.finish() + + # Write out the new timing data + if result == os.EX_OK: + write_timing(timingfile) + + # Return the build result + return result + + +# Variables used by add_line: +search_last = 0 +search_wait = 0 +search_max = 0 + +def add_line(line, timing, timing_index, master_file, mysettings): + """ + Process a line of output from the build subprocess. + + @param line: The line of text, without any trailing newline. + @type line: String + @param timing: The timing template, or None if not available. + @type timing: Array + @param timing_index: The current index into the timing template. + @type timing_index: Integer + @param master_file: Pseudo-TTY from which build output is read. + @type master_file: Filehandle + @param mysettings: Portage configuration. + @type mysettings: Instance of portage.config + @returns The new timing index, or 0 if no template is available. + """ + + global timing_output, timing_outtimes, timing_outpos + + now = time.time() + + # First, append the line to the output arrays. Massage the line a bit + # so that changes in version number, compilation options and the like + # don't cause mismatches later on. + package, version = pkgsplit(mysettings.mycpv.split("/",1)[1])[0:2] + line = re.sub(r'\s+', r' ', line) + line = line.replace(package+'-'+version, package+'-VER') + line = re.sub(r'(^|\W)(cc|gcc|xgcc|gcc3|icc)(\W)', r'\1CC\3', line) + line = re.sub(r'(^|\W)(c\+\+|g\+\+|g\+\+3)(\W)', r'\1CXX\3', line) + if re.match(r'^\s*(cd\s+\S+\s*;\s*)?(if\s+)?(stage\d\/)?(CC|CXX)[^=]', line): + line = re.sub(r'(\s+-(-\S+|[^-\s]\S*))+', r' -OPTS', line) + if timing_outpos >= len(timing_output): + # Extend the arrays in big jumps for efficiency + timing_output.append([None]*1000) + timing_outtimes.append([None]*1000) + timing_output[timing_outpos] = line + timing_outtimes[timing_outpos] = now + timing_outpos += 1 + + # Now look for a matching line in the template (if we don't have a + # template, just return 0). + if not timing: + return 0 + global search_last # Time of last search + global search_wait # Time in seconds to wait before the next search + global search_max # Maximum time in seconds to search + # If the next line matches, use it. + if timing[timing_index+1][1] == line: + search_wait = 0 + search_max = 0.05 + return timing_index+1 + # Otherwise we have to search. But don't do so on every line, or we + # could eat huge amounts of CPU time if the build output differs from + # the template. + if time.time() - search_last < search_wait: + return timing_index + # Search forward for a matching line until we hit our time limit, but + # don't keep the build process waiting if it has more output for us. + found = False + got_output = False + search_limit = time.time() + search_max + index = timing_index + 2 + while index < len(timing)-1 and time.time() - search_limit: + if timing[index][1] == line: + found = True + break + if not got_output and select.select([master_file], [], [], 0): + got_output = True + search_limit = time.time() + 0.01 + index += 1 + # Save the time this search ended. + search_last = time.time() + # If we found a match, return it; otherwise try harder next time. + if found: + search_wait = 0; + search_max = 0.05; + return index + else: + search_wait += 0.05 + if search_wait > 3: + search_wait = 3 + search_max += 0.05 + if search_wait > 1: + search_wait = 1 + return timing_index