def async_job(verb, job, threadpool, locks, event_queue, log_path): """Run a sequence of Stages from a Job and collect their output. :param job: A Job instance :threadpool: A thread pool executor for blocking stages :event_queue: A queue for asynchronous events """ # Initialize success flag all_stages_succeeded = True # Jobs start occuping a jobserver job occupying_job = True # Load environment for this job job_env = job.getenv(os.environ) # Execute each stage of this job for stage in job.stages: # Logger reference in this scope for error reporting logger = None # Abort the job if one of the stages has failed if job.continue_on_failure and not all_stages_succeeded: break # Check for stage synchronization lock if stage.locked_resource is not None: lock = locks.setdefault(stage.locked_resource, asyncio.Lock()) yield asyncio.From(lock) else: lock = FakeLock() try: # If the stage doesn't require a job token, release it temporarily if stage.occupy_job: if not occupying_job: while job_server.try_acquire() is None: yield asyncio.From(asyncio.sleep(0.05)) occupying_job = True else: if occupying_job: job_server.release() occupying_job = False # Notify stage started event_queue.put(ExecutionEvent( 'STARTED_STAGE', job_id=job.jid, stage_label=stage.label)) if type(stage) is CommandStage: try: # Initiate the command while True: try: # Update the environment for this stage (respects overrides) stage.update_env(job_env) # Get the logger protocol_type = stage.logger_factory(verb, job.jid, stage.label, event_queue, log_path) # Start asynchroonous execution transport, logger = yield asyncio.From( async_execute_process( protocol_type, **stage.async_execute_process_kwargs)) break except OSError as exc: if 'Text file busy' in str(exc): # This is a transient error, try again shortly # TODO: report the file causing the problem (exc.filename) yield asyncio.From(asyncio.sleep(0.01)) continue raise # Notify that a subprocess has been created event_queue.put(ExecutionEvent( 'SUBPROCESS', job_id=job.jid, stage_label=stage.label, stage_repro=stage.get_reproduction_cmd(verb, job.jid), **stage.async_execute_process_kwargs)) # Asynchronously yield until this command is completed retcode = yield asyncio.From(logger.complete) except: logger = IOBufferLogger(verb, job.jid, stage.label, event_queue, log_path) logger.err(str(traceback.format_exc())) retcode = 3 elif type(stage) is FunctionStage: logger = IOBufferLogger(verb, job.jid, stage.label, event_queue, log_path) try: # Asynchronously yield until this function is completed retcode = yield asyncio.From(get_loop().run_in_executor( threadpool, stage.function, logger, event_queue)) except: logger.err('Stage `{}` failed with arguments:'.format(stage.label)) for arg_val in stage.args: logger.err(' {}'.format(arg_val)) for arg_name, arg_val in stage.kwargs.items(): logger.err(' {}: {}'.format(arg_name, arg_val)) logger.err(str(traceback.format_exc())) retcode = 3 else: raise TypeError("Bad Job Stage: {}".format(stage)) # Set whether this stage succeeded stage_succeeded = (retcode == 0) # Update success tracker from this stage all_stages_succeeded = all_stages_succeeded and stage_succeeded # Store the results from this stage event_queue.put(ExecutionEvent( 'FINISHED_STAGE', job_id=job.jid, stage_label=stage.label, succeeded=stage_succeeded, stdout=logger.get_stdout_log(), stderr=logger.get_stderr_log(), interleaved=logger.get_interleaved_log(), logfile_filename=logger.unique_logfile_name, repro=stage.get_reproduction_cmd(verb, job.jid), retcode=retcode)) # Close logger logger.close() finally: lock.release() # Finally, return whether all stages of the job completed raise asyncio.Return(job.jid, all_stages_succeeded)
def build_isolated_workspace(context, packages=None, start_with=None, no_deps=False, unbuilt=False, n_jobs=None, force_cmake=False, pre_clean=False, force_color=False, quiet=False, interleave_output=False, no_status=False, limit_status_rate=10.0, lock_install=False, no_notify=False, continue_on_failure=False, summarize_build=None, relaxed_constraints=False, influx_url=None, influx_db=None): """Builds a catkin workspace in isolation This function will find all of the packages in the source space, start some executors, feed them packages to build based on dependencies and topological ordering, and then monitor the output of the executors, handling loggings of the builds, starting builds, failing builds, and finishing builds of packages, and handling the shutdown of the executors when appropriate. :param context: context in which to build the catkin workspace :type context: :py:class:`catkin_tools.verbs.catkin_build.context.Context` :param packages: list of packages to build, by default their dependencies will also be built :type packages: list :param start_with: package to start with, skipping all packages which proceed it in the topological order :type start_with: str :param no_deps: If True, the dependencies of packages will not be built first :type no_deps: bool :param n_jobs: number of parallel package build n_jobs :type n_jobs: int :param force_cmake: forces invocation of CMake if True, default is False :type force_cmake: bool :param force_color: forces colored output even if terminal does not support it :type force_color: bool :param quiet: suppresses the output of commands unless there is an error :type quiet: bool :param interleave_output: prints the output of commands as they are received :type interleave_output: bool :param no_status: disables status bar :type no_status: bool :param limit_status_rate: rate to which status updates are limited; the default 0, places no limit. :type limit_status_rate: float :param lock_install: causes executors to synchronize on access of install commands :type lock_install: bool :param no_notify: suppresses system notifications :type no_notify: bool :param continue_on_failure: do not stop building other jobs on error :type continue_on_failure: bool :param summarize_build: if True summarizes the build at the end, if None and continue_on_failure is True and the the build fails, then the build will be summarized, but if False it never will be summarized. :type summarize_build: bool :param relaxed_constraints If true, do not use exec_deps for topological ordering :type relaxed_constraints bool :param influx_url Url to access an InfluxDB instance :type influx_url string Url of the form user:password@host:port :param influx_db Database name in InfluxDB :type influx_db string :raises: SystemExit if buildspace is a file or no packages were found in the source space or if the provided options are invalid """ pre_start_time = time.time() # Assert that the limit_status_rate is valid if limit_status_rate < 0: sys.exit( "[build] @!@{rf}Error:@| The value of --status-rate must be greater than or equal to zero." ) # Declare a buildspace marker describing the build config for error checking buildspace_marker_data = { 'workspace': context.workspace, 'profile': context.profile, 'install': context.install, 'install_space': context.install_space_abs, 'devel_space': context.devel_space_abs, 'source_space': context.source_space_abs } # Check build config if os.path.exists( os.path.join(context.build_space_abs, BUILDSPACE_MARKER_FILE)): with open(os.path.join( context.build_space_abs, BUILDSPACE_MARKER_FILE)) as buildspace_marker_file: existing_buildspace_marker_data = yaml.safe_load( buildspace_marker_file) misconfig_lines = '' for (k, v) in existing_buildspace_marker_data.items(): new_v = buildspace_marker_data.get(k, None) if new_v != v: misconfig_lines += ( '\n - %s: %s (stored) is not %s (commanded)' % (k, v, new_v)) if len(misconfig_lines) > 0: sys.exit( clr("\n@{rf}Error:@| Attempting to build a catkin workspace using build space: " "\"%s\" but that build space's most recent configuration " "differs from the commanded one in ways which will cause " "problems. Fix the following options or use @{yf}`catkin " "clean -b`@| to remove the build space: %s" % (context.build_space_abs, misconfig_lines))) # Summarize the context summary_notes = [] if force_cmake: summary_notes += [ clr("@!@{cf}NOTE:@| Forcing CMake to run for each package.") ] log(context.summary(summary_notes)) # Make sure there is a build folder and it is not a file if os.path.exists(context.build_space_abs): if os.path.isfile(context.build_space_abs): sys.exit( clr("[build] @{rf}Error:@| Build space '{0}' exists but is a file and not a folder." .format(context.build_space_abs))) # If it dosen't exist, create it else: log("[build] Creating build space: '{0}'".format( context.build_space_abs)) os.makedirs(context.build_space_abs) # Write the current build config for config error checking with open(os.path.join(context.build_space_abs, BUILDSPACE_MARKER_FILE), 'w') as buildspace_marker_file: buildspace_marker_file.write( yaml.dump(buildspace_marker_data, default_flow_style=False)) # Get all the packages in the context source space # Suppress warnings since this is a utility function workspace_packages = find_packages(context.source_space_abs, exclude_subspaces=True, warnings=[]) # Get packages which have not been built yet built_packages, unbuilt_pkgs = get_built_unbuilt_packages( context, workspace_packages) # Handle unbuilt packages if unbuilt: # Check if there are any unbuilt if len(unbuilt_pkgs) > 0: # Add the unbuilt packages packages.extend(list(unbuilt_pkgs)) else: log("[build] No unbuilt packages to be built.") return # If no_deps is given, ensure packages to build are provided if no_deps and packages is None: log( clr("[build] @!@{rf}Error:@| With no_deps, you must specify packages to build." )) return # Find list of packages in the workspace packages_to_be_built, packages_to_be_built_deps, all_packages = determine_packages_to_be_built( packages, context, workspace_packages) if not no_deps: # Extend packages to be built to include their deps packages_to_be_built.extend(packages_to_be_built_deps) # Also re-sort try: packages_to_be_built = topological_order_packages( dict(packages_to_be_built)) except AttributeError: log( clr("[build] @!@{rf}Error:@| The workspace packages have a circular " "dependency, and cannot be built. Please run `catkin list " "--deps` to determine the problematic package(s).")) return # Check the number of packages to be built if len(packages_to_be_built) == 0: log(clr('[build] No packages to be built.')) # Assert start_with package is in the workspace verify_start_with_option(start_with, packages, all_packages, packages_to_be_built) # Populate .catkin file if we're not installing # NOTE: This is done to avoid the Catkin CMake code from doing it, # which isn't parallel-safe. Catkin CMake only modifies this file if # it's package source path isn't found. if not context.install: dot_catkin_file_path = os.path.join(context.devel_space_abs, '.catkin') # If the file exists, get the current paths if os.path.exists(dot_catkin_file_path): dot_catkin_paths = open(dot_catkin_file_path, 'r').read().split(';') else: dot_catkin_paths = [] # Update the list with the new packages (in topological order) packages_to_be_built_paths = [ os.path.join(context.source_space_abs, path) for path, pkg in packages_to_be_built ] new_dot_catkin_paths = [ os.path.join(context.source_space_abs, path) for path in [ os.path.join(context.source_space_abs, path) for path, pkg in all_packages ] if path in dot_catkin_paths or path in packages_to_be_built_paths ] # Write the new file if it's different, otherwise, leave it alone if dot_catkin_paths == new_dot_catkin_paths: wide_log("[build] Package table is up to date.") else: wide_log("[build] Updating package table.") open(dot_catkin_file_path, 'w').write(';'.join(new_dot_catkin_paths)) # Remove packages before start_with if start_with is not None: for path, pkg in list(packages_to_be_built): if pkg.name != start_with: wide_log( clr("@!@{pf}Skipping@| @{gf}---@| @{cf}{}@|").format( pkg.name)) packages_to_be_built.pop(0) else: break # Get the names of all packages to be built packages_to_be_built_names = [p.name for _, p in packages_to_be_built] packages_to_be_built_deps_names = [ p.name for _, p in packages_to_be_built_deps ] # Generate prebuild and prebuild clean jobs, if necessary prebuild_jobs = {} setup_util_present = os.path.exists( os.path.join(context.devel_space_abs, '_setup_util.py')) catkin_present = 'catkin' in (packages_to_be_built_names + packages_to_be_built_deps_names) catkin_built = 'catkin' in built_packages prebuild_built = 'catkin_tools_prebuild' in built_packages # Handle the prebuild jobs if the develspace is linked prebuild_pkg_deps = [] if context.link_devel: prebuild_pkg = None # Construct a dictionary to lookup catkin package by name pkg_dict = dict([(pkg.name, (pth, pkg)) for pth, pkg in all_packages]) if setup_util_present: # Setup util is already there, determine if it needs to be # regenerated if catkin_built: if catkin_present: prebuild_pkg_path, prebuild_pkg = pkg_dict['catkin'] elif prebuild_built: if catkin_present: # TODO: Clean prebuild package ct_prebuild_pkg_path = get_prebuild_package( context.build_space_abs, context.devel_space_abs, force_cmake) ct_prebuild_pkg = parse_package(ct_prebuild_pkg_path) prebuild_jobs[ 'caktin_tools_prebuild'] = create_catkin_clean_job( context, ct_prebuild_pkg, ct_prebuild_pkg_path, dependencies=[], dry_run=False, clean_build=True, clean_devel=True, clean_install=True) # TODO: Build catkin package prebuild_pkg_path, prebuild_pkg = pkg_dict['catkin'] prebuild_pkg_deps.append('catkin_tools_prebuild') else: # How did these get here?? log("Warning: devel space setup files have an unknown origin.") else: # Setup util needs to be generated if catkin_built or prebuild_built: log("Warning: generated devel space setup files have been deleted." ) if catkin_present: # Build catkin package prebuild_pkg_path, prebuild_pkg = pkg_dict['catkin'] else: # Generate and buildexplicit prebuild package prebuild_pkg_path = get_prebuild_package( context.build_space_abs, context.devel_space_abs, force_cmake) prebuild_pkg = parse_package(prebuild_pkg_path) if prebuild_pkg is not None: # Create the prebuild job prebuild_job = create_catkin_build_job( context, prebuild_pkg, prebuild_pkg_path, build_dependencies=prebuild_pkg_deps, run_dependencies=[], force_cmake=force_cmake, pre_clean=pre_clean, prebuild=True) # Add the prebuld job prebuild_jobs[prebuild_job.jid] = prebuild_job # Remove prebuild jobs from normal job list for prebuild_jid, prebuild_job in prebuild_jobs.items(): if prebuild_jid in packages_to_be_built_names: packages_to_be_built_names.remove(prebuild_jid) # Initial jobs list is just the prebuild jobs jobs = [] + list(prebuild_jobs.values()) # Get all build type plugins build_job_creators = { ep.name: ep.load()['create_build_job'] for ep in pkg_resources.iter_entry_points(group='catkin_tools.jobs') } # It's a problem if there aren't any build types available if len(build_job_creators) == 0: sys.exit( 'Error: No build types available. Please check your catkin_tools installation.' ) # Construct jobs for pkg_path, pkg in all_packages: if pkg.name not in packages_to_be_built_names: continue # Ignore metapackages if 'metapackage' in [e.tagname for e in pkg.exports]: continue # Get actual execution deps build_deps = [ p.name for _, p in get_cached_recursive_build_depends_in_workspace( pkg, packages_to_be_built) if p.name not in prebuild_jobs ] build_for_run_deps = [ p.name for _, p in get_cached_recursive_run_depends_in_workspace( pkg, packages_to_be_built) if p.name not in prebuild_jobs ] # All jobs depend on the prebuild jobs if they're defined if not no_deps: if relaxed_constraints: build_for_run_deps = [ p.name for _, p in get_recursive_build_depends_for_run_depends_in_workspace( [pkg], packages_to_be_built) if p.name not in prebuild_jobs ] else: # revert to interpreting all dependencies as build dependencies build_deps = list(set(build_deps + build_for_run_deps)) build_for_run_deps = [] for j in prebuild_jobs.values(): build_deps.append(j.jid) # Determine the job parameters build_job_kwargs = dict(context=context, package=pkg, package_path=pkg_path, build_dependencies=build_deps, run_dependencies=build_for_run_deps, force_cmake=force_cmake, pre_clean=pre_clean) # Create the job based on the build type build_type = get_build_type(pkg) if build_type in build_job_creators: jobs.append(build_job_creators[build_type](**build_job_kwargs)) else: wide_log( clr("[build] @!@{yf}Warning:@| Skipping package `{}` because it " "has an unsupported package build type: `{}`").format( pkg.name, build_type)) wide_log(clr("[build] Note: Available build types:")) for bt_name in build_job_creators.keys(): wide_log(clr("[build] - `{}`".format(bt_name))) # Queue for communicating status event_queue = Queue() status_queue = Queue() monitoring_queue = Queue() class ForwardingQueue(threading.Thread): def __init__(self, queues): super(ForwardingQueue, self).__init__() self.keep_running = True self.queues = queues def run(self): while self.keep_running: event = event_queue.get(True) for queue in self.queues: queue.put(event) if event is None: break queue_thread = ForwardingQueue([status_queue, monitoring_queue]) threads = [queue_thread] try: # Spin up status output thread status_thread = ConsoleStatusController( 'build', ['package', 'packages'], jobs, n_jobs, [pkg.name for _, pkg in context.packages], [p for p in context.whitelist], [p for p in context.blacklist], status_queue, show_notifications=not no_notify, show_active_status=not no_status, show_buffered_stdout=not quiet and not interleave_output, show_buffered_stderr=not interleave_output, show_live_stdout=interleave_output, show_live_stderr=interleave_output, show_stage_events=not quiet, show_full_summary=(summarize_build is True), pre_start_time=pre_start_time, active_status_rate=limit_status_rate) threads.append(status_thread) if influx_db is not None: if not have_influx_db: sys.exit( "[build] @!@{rf}Error:@| InfluxDB monitoring is not possible, cannot import influxdb" ) match = re.match('^(.+):(.+)@(.+):(.+)$', influx_url) if not match: sys.exit( "[build] @!@{rf}Error:@| The value of --influx has to be of the form username:password@host:port" ) username, password, host, port = match.groups() influxdb_thread = InfluxDBStatusController(monitoring_queue, influx_db, host, port, username, password) threads.append(influxdb_thread) for thread in threads: thread.start() # Initialize locks locks = { 'installspace': asyncio.Lock() if lock_install else FakeLock() } # Block while running N jobs asynchronously try: all_succeeded = run_until_complete( execute_jobs('build', jobs, locks, event_queue, context.log_space_abs, max_toplevel_jobs=n_jobs, continue_on_failure=continue_on_failure, continue_without_deps=False, relaxed_constraints=relaxed_constraints)) except Exception: all_succeeded = False for thread in threads: thread.keep_running = False for thread in threads: thread.join(1.0) wide_log(str(traceback.format_exc())) event_queue.put(None) for thread in threads: thread.join(1.0) # Warn user about new packages now_built_packages, now_unbuilt_pkgs = get_built_unbuilt_packages( context, workspace_packages) new_pkgs = [p for p in unbuilt_pkgs if p not in now_unbuilt_pkgs] if len(new_pkgs) > 0: log( clr("[build] @/@!Note:@| @/Workspace packages have changed, " "please re-source setup files to use them.@|")) if all_succeeded: # Create isolated devel setup if necessary if context.isolate_devel: if not context.install: _create_unmerged_devel_setup(context, now_unbuilt_pkgs) else: _create_unmerged_devel_setup_for_install(context) return 0 else: return 1 except KeyboardInterrupt: wide_log("[build] Interrupted by user!") event_queue.put(None) return 130 # EOWNERDEAD return code is not part of the errno module.
def async_job(verb, job, threadpool, locks, event_queue, log_path): """Run a sequence of Stages from a Job and collect their output. :param job: A Job instance :threadpool: A thread pool executor for blocking stages :event_queue: A queue for asynchronous events """ # Initialize success flag all_stages_succeeded = True # Jobs start occuping a jobserver job occupying_job = True # Execute each stage of this job for stage in job.stages: # Logger reference in this scope for error reporting logger = None # Abort the job if one of the stages has failed if job.continue_on_failure and not all_stages_succeeded: break # Check for stage synchronization lock if stage.locked_resource is not None: lock = locks.setdefault(stage.locked_resource, asyncio.Lock()) yield from lock else: lock = FakeLock() try: # If the stage doesn't require a job token, release it temporarily if stage.occupy_job: if not occupying_job: while job_server.try_acquire() is None: yield from asyncio.sleep(0.05) occupying_job = True else: if occupying_job: job_server.release() occupying_job = False # Notify stage started event_queue.put( ExecutionEvent('STARTED_STAGE', job_id=job.jid, stage_label=stage.label)) if type(stage) is CommandStage: try: # Initiate the command while True: try: # Update the environment for this stage (respects overrides) stage.update_env(job.env) # Get the logger protocol_type = stage.logger_factory( verb, job.jid, stage.label, event_queue, log_path) # Start asynchroonous execution transport, logger = yield from ( async_execute_process( protocol_type, **stage.async_execute_process_kwargs)) break except OSError as exc: if 'Text file busy' in str(exc): # This is a transient error, try again shortly # TODO: report the file causing the problem (exc.filename) yield from asyncio.sleep(0.01) continue raise # Notify that a subprocess has been created event_queue.put( ExecutionEvent('SUBPROCESS', job_id=job.jid, stage_label=stage.label, stage_repro=stage.get_reproduction_cmd( verb, job.jid), **stage.async_execute_process_kwargs)) # Asynchronously yield until this command is completed retcode = yield from logger.complete except: # noqa: E722 # Bare except is permissable here because the set of errors which the CommandState might raise # is unbounded. We capture the traceback here and save it to the build's log files. logger = IOBufferLogger(verb, job.jid, stage.label, event_queue, log_path) logger.err(str(traceback.format_exc())) retcode = 3 elif type(stage) is FunctionStage: logger = IOBufferLogger(verb, job.jid, stage.label, event_queue, log_path) try: # Asynchronously yield until this function is completed retcode = yield from get_loop().run_in_executor( threadpool, stage.function, logger, event_queue) except: # noqa: E722 # Bare except is permissable here because the set of errors which the FunctionStage might raise # is unbounded. We capture the traceback here and save it to the build's log files. logger.err('Stage `{}` failed with arguments:'.format( stage.label)) for arg_val in stage.args: logger.err(' {}'.format(arg_val)) for arg_name, arg_val in stage.kwargs.items(): logger.err(' {}: {}'.format(arg_name, arg_val)) retcode = 3 else: raise TypeError("Bad Job Stage: {}".format(stage)) # Set whether this stage succeeded stage_succeeded = (retcode == 0) # Update success tracker from this stage all_stages_succeeded = all_stages_succeeded and stage_succeeded # Store the results from this stage event_queue.put( ExecutionEvent('FINISHED_STAGE', job_id=job.jid, stage_label=stage.label, succeeded=stage_succeeded, stdout=logger.get_stdout_log(), stderr=logger.get_stderr_log(), interleaved=logger.get_interleaved_log(), logfile_filename=logger.unique_logfile_name, repro=stage.get_reproduction_cmd(verb, job.jid), retcode=retcode)) # Close logger logger.close() finally: lock.release() # Finally, return whether all stages of the job completed return (job.jid, all_stages_succeeded)
def build_isolated_workspace(context, packages=None, start_with=None, no_deps=False, jobs=None, force_cmake=False, force_color=False, quiet=False, interleave_output=False, no_status=False, lock_install=False, no_notify=False): """Builds a catkin workspace in isolation This function will find all of the packages in the source space, start some executors, feed them packages to build based on dependencies and topological ordering, and then monitor the output of the executors, handling loggings of the builds, starting builds, failing builds, and finishing builds of packages, and handling the shutdown of the executors when appropriate. :param context: context in which to build the catkin workspace :type context: :py:class:`catkin_tools.verbs.catkin_build.context.Context` :param packages: list of packages to build, by default their dependencies will also be built :type packages: list :param start_with: package to start with, skipping all packages which proceed it in the topological order :type start_with: str :param no_deps: If True, the dependencies of packages will not be built first :type no_deps: bool :param jobs: number of parallel package build jobs :type jobs: int :param force_cmake: forces invocation of CMake if True, default is False :type force_cmake: bool :param force_color: forces colored output even if terminal does not support it :type force_color: bool :param quiet: suppresses the output of commands unless there is an error :type quiet: bool :param interleave_output: prints the output of commands as they are received :type interleave_output: bool :param no_status: disables status bar :type no_status: bool :param lock_install: causes executors to synchronize on access of install commands :type lock_install: bool :param no_notify: suppresses system notifications :type no_notify: bool :raises: SystemExit if buildspace is a file or no packages were found in the source space or if the provided options are invalid """ # If no_deps is given, ensure packages to build are provided if no_deps and packages is None: sys.exit("With --no-deps, you must specify packages to build.") # Make sure there is a build folder and it is not a file if os.path.exists(context.build_space_abs): if os.path.isfile(context.build_space_abs): sys.exit( clr("@{rf}Error:@| Build space '{0}' exists but is a file and not a folder." .format(context.build_space_abs))) # If it dosen't exist, create it else: log("Creating build space directory, '{0}'".format( context.build_space_abs)) os.makedirs(context.build_space_abs) # Check for catkin_make droppings if context.corrupted_by_catkin_make(): sys.exit( clr("@{rf}Error:@| Build space `{0}` exists but appears to have previously been " "created by the `catkin_make` or `catkin_make_isolated` tool. " "Please choose a different directory to use with `catkin build` " "or clean the build space.".format(context.build_space_abs))) # Declare a buildspace marker describing the build config for error checking buildspace_marker_data = { 'workspace': context.workspace, 'profile': context.profile, 'install': context.install, 'install_space': context.install_space_abs, 'devel_space': context.devel_space_abs, 'source_space': context.source_space_abs } # Check build config if os.path.exists( os.path.join(context.build_space_abs, BUILDSPACE_MARKER_FILE)): with open(os.path.join( context.build_space_abs, BUILDSPACE_MARKER_FILE)) as buildspace_marker_file: existing_buildspace_marker_data = yaml.load(buildspace_marker_file) misconfig_lines = '' for (k, v) in existing_buildspace_marker_data.items(): new_v = buildspace_marker_data.get(k, None) if new_v != v: misconfig_lines += ( '\n - %s: %s (stored) is not %s (commanded)' % (k, v, new_v)) if len(misconfig_lines) > 0: sys.exit( clr("\n@{rf}Error:@| Attempting to build a catkin workspace using build space: " "\"%s\" but that build space's most recent configuration " "differs from the commanded one in ways which will cause " "problems. Fix the following options or use @{yf}`catkin " "clean -b`@| to remove the build space: %s" % (context.build_space_abs, misconfig_lines))) # Write the current build config for config error checking with open(os.path.join(context.build_space_abs, BUILDSPACE_MARKER_FILE), 'w') as buildspace_marker_file: buildspace_marker_file.write( yaml.dump(buildspace_marker_data, default_flow_style=False)) # Summarize the context summary_notes = [] if force_cmake: summary_notes += [ clr("@!@{cf}NOTE:@| Forcing CMake to run for each package.") ] log(context.summary(summary_notes)) # Find list of packages in the workspace packages_to_be_built, packages_to_be_built_deps, all_packages = determine_packages_to_be_built( packages, context) completed_packages = [] if no_deps: # Consider deps as "completed" completed_packages.extend(packages_to_be_built_deps) else: # Extend packages to be built to include their deps packages_to_be_built.extend(packages_to_be_built_deps) # Also resort packages_to_be_built = topological_order_packages( dict(packages_to_be_built)) max_package_name_length = max( [len(pkg.name) for pth, pkg in packages_to_be_built]) # Assert start_with package is in the workspace verify_start_with_option(start_with, packages, all_packages, packages_to_be_built + packages_to_be_built_deps) # Setup pool of executors executors = {} # The communication queue can have ExecutorEvent's or str's passed into it from the executors comm_queue = Queue() # The job queue has Jobs put into it job_queue = Queue() # Lock for install space install_lock = Lock() if lock_install else FakeLock() # Determine the number of executors try: if jobs: jobs = int(jobs) if jobs < 1: sys.exit( "Specified number of jobs '{0}' is not positive.".format( jobs)) except ValueError: sys.exit("Specified number of jobs '{0}' is no integer.".format(jobs)) try: jobs = cpu_count() if jobs is None else jobs except NotImplementedError: log('Failed to determine the cpu_count, falling back to 1 jobs as the default.' ) jobs = 1 if jobs is None else jobs # If only one set of jobs, turn on interleaving to get more responsive feedback if jobs == 1: # TODO: make the system more intelligent so that it can automatically switch to streaming output # when only one job is building, even if multiple jobs could be building quiet = False interleave_output = True # Start the executors for x in range(jobs): e = Executor(x, context, comm_queue, job_queue, install_lock) executors[x] = e e.start() try: # Finally close out now running executors # Variables for tracking running jobs and built/building packages start = time.time() total_packages = len(packages_to_be_built) package_count = 0 running_jobs = {} log_dir = os.path.join(context.build_space_abs, 'build_logs') color = True if not force_color and not is_tty(sys.stdout): color = True out = OutputController(log_dir, quiet, interleave_output, color, max_package_name_length, prefix_output=(jobs > 1)) if no_status: disable_wide_log() # Prime the job_queue ready_packages = [] if start_with is None: ready_packages = get_ready_packages(packages_to_be_built, running_jobs, completed_packages) while start_with is not None: ready_packages.extend( get_ready_packages(packages_to_be_built, running_jobs, completed_packages)) while ready_packages: pth, pkg = ready_packages.pop(0) if pkg.name != start_with: completed_packages.append(pkg.name) package_count += 1 wide_log("[build] Skipping package '{0}'".format(pkg.name)) else: ready_packages.insert(0, (pth, pkg)) start_with = None break running_jobs = queue_ready_packages(ready_packages, running_jobs, job_queue, context, force_cmake) assert running_jobs error_state = False errors = [] def set_error_state(error_state): if error_state: return # Set the error state to prevent new jobs error_state = True # Empty the job queue while not job_queue.empty(): job_queue.get() # Kill the executors by sending a None to the job queue for each of them for x in range(jobs): job_queue.put(None) # While any executors are running, process executor events while executors: try: # Try to get an event from the communications queue try: event = comm_queue.get(True, 0.1) except Empty: # timeout occured, create null event to pass through checks event = ExecutorEvent(None, None, None, None) if event.event_type == 'job_started': package_count += 1 running_jobs[ event.package]['package_number'] = package_count running_jobs[event.package]['start_time'] = time.time() out.job_started(event.package) if event.event_type == 'command_started': out.command_started(event.package, event.data['cmd'], event.data['location']) if event.event_type == 'command_log': out.command_log(event.package, event.data['message']) if event.event_type == 'command_failed': out.command_failed(event.package, event.data['cmd'], event.data['location'], event.data['retcode']) # Add to list of errors errors.append(event) # Remove the command from the running jobs del running_jobs[event.package] # If it hasn't already been done, stop the executors set_error_state(error_state) if event.event_type == 'command_finished': out.command_finished(event.package, event.data['cmd'], event.data['location'], event.data['retcode']) if event.event_type == 'job_finished': completed_packages.append(event.package) run_time = format_time_delta( time.time() - running_jobs[event.package]['start_time']) out.job_finished(event.package, run_time) del running_jobs[event.package] # If shutting down, do not add new packages if error_state: continue # Calculate new packages if not no_status: wide_log('[build] Calculating new jobs...', end='\r') sys.stdout.flush() ready_packages = get_ready_packages( packages_to_be_built, running_jobs, completed_packages) running_jobs = queue_ready_packages( ready_packages, running_jobs, job_queue, context, force_cmake) # Make sure there are jobs to be/being processed, otherwise kill the executors if not running_jobs: # Kill the executors by sending a None to the job queue for each of them for x in range(jobs): job_queue.put(None) # If an executor exit event, join it and remove it from the executors list if event.event_type == 'exit': # If an executor has an exception, set the error state if event.data['reason'] == 'exception': set_error_state(error_state) errors.append(event) # Join and remove it executors[event.executor_id].join() del executors[event.executor_id] if not no_status: # Update the status bar on the screen executing_jobs = [] for name, value in running_jobs.items(): number, job, start_time = value[ 'package_number'], value['job'], value[ 'start_time'] if number is None or start_time is None: continue executing_jobs.append({ 'number': number, 'name': name, 'run_time': format_time_delta_short(time.time() - start_time) }) msg = clr("[build - {run_time}] ").format( run_time=format_time_delta_short(time.time() - start)) # If errors post those if errors: for error in errors: msg += clr("[!{package}] ").format( package=error.package) # Print them in order of started number for job_msg_args in sorted( executing_jobs, key=lambda args: args['number']): msg += clr("[{name} - {run_time}] ").format( **job_msg_args) msg_rhs = clr( "[{0}/{1} Active | {2}/{3} Completed]").format( len(executing_jobs), len(executors), len(packages) if no_deps else len(completed_packages), total_packages) # Update title bar sys.stdout.write("\x1b]2;[build] {0}/{1}\x07".format( len(packages) if no_deps else len(completed_packages), total_packages)) # Update status bar wide_log(msg, rhs=msg_rhs, end='\r') sys.stdout.flush() except KeyboardInterrupt: wide_log("[build] User interrupted, stopping.") set_error_state(error_state) # All executors have shutdown sys.stdout.write("\x1b]2;\x07") if not errors: if context.isolate_devel: if not context.install: _create_unmerged_devel_setup(context) else: _create_unmerged_devel_setup_for_install(context) wide_log("[build] Finished.") if not no_notify: notify("Build Finished", "{0} packages built".format(total_packages)) return 0 else: wide_log(clr("[build] There were @!@{rf}errors@|:")) if not no_notify: notify("Build Failed", "there were {0} errors".format(len(errors))) for error in errors: if error.event_type == 'exit': wide_log( """Executor '{exec_id}' had an unhandle exception while processing package '{package}': {data[exc]} """.format(exec_id=error.executor_id + 1, **error.__dict__)) else: wide_log( clr(""" @{rf}Failed@| to build package '@{cf}{package}@|' because the following command: @!@{kf}# Command run in directory: @|{location} {cmd.cmd_str} @{rf}Exited@| with return code: @!{retcode}@|""").format( package=error.package, **error.data)) sys.exit(1) finally: # Ensure executors go down for x in range(jobs): job_queue.put(None)