#!/usr/bin/env python3 # Donate CPU # # A script a user can run to donate CPU to cppcheck project # # Syntax: donate-cpu.py [-jN] [--package=url] [--stop-time=HH:MM] [--work-path=path] [--test] [--bandwidth-limit=limit] # -jN Use N threads in compilation/analysis. Default is 1. # --package=url Check a specific package and then stop. Can be useful if you want to reproduce # some warning/crash/exception/etc.. # --stop-time=HH:MM Stop analysis when time has passed. Default is that you must terminate the script. # --work-path=path Work folder path. Default path is cppcheck-donate-cpu-workfolder in your home folder. # --test Connect to a donate-cpu-server that is running locally on port 8001 for testing. # --bandwidth-limit=limit Limit download rate for packages. Format for limit is the same that wget uses. # Examples: --bandwidth-limit=250k => max. 250 kilobytes per second # --bandwidth-limit=2m => max. 2 megabytes per second # --max-packages=N Process N packages and then exit. A value of 0 means infinitely. # --no-upload Do not upload anything. Defaults to False. # --packages Process a list of given packages. # --version Returns the version (of the underlying donate_cpu_lib.py). # # What this script does: # 1. Check requirements # 2. Pull & compile Cppcheck # 3. Select a package # 4. Download package # 5. Analyze source code # 6. Upload results # 7. Repeat from step 2 # # Quick start: just run this script without any arguments import platform from packaging.version import Version from donate_cpu_lib import * max_packages = None package_urls = [] for arg in sys.argv[1:]: # --stop-time=12:00 => run until ~12:00 and then stop if arg.startswith('--stop-time='): stop_time = arg[-5:] print('Stop time:' + stop_time) elif arg.startswith('-j'): if not re.match(r'-j\d+', arg): print('Argument "{}" is invalid.'.format(arg)) print('"-j" must be followed by a positive number.') sys.exit(1) jobs = arg print('Jobs:' + jobs[2:]) elif arg.startswith('--package='): pkg = arg[arg.find('=')+1:] package_urls.append(pkg) print('Added Package:' + pkg) elif arg.startswith('--packages='): pkg_cnt = len(package_urls) with open(arg[arg.find('=')+1:], 'rt') as f: for package_url in f: package_url = package_url.strip() if not package_url: continue package_urls.append(package_url) print('Added Packages:' + str(len(package_urls) - pkg_cnt)) elif arg.startswith('--work-path='): work_path = os.path.abspath(arg[arg.find('=')+1:]) print('work_path:' + work_path) if not os.path.exists(work_path): print('work path does not exist!') sys.exit(1) elif arg == '--test': server_address = ('localhost', 8001) elif arg.startswith('--bandwidth-limit='): bandwidth_limit = arg[arg.find('=')+1:] elif arg.startswith('--max-packages='): arg_value = arg[arg.find('=')+1:] try: max_packages = int(arg_value) except ValueError: max_packages = None if max_packages < 0: max_packages = None if max_packages is None: print('Error: Max. packages value "{}" is invalid. Must be a positive number or 0.'.format(arg_value)) sys.exit(1) # 0 means infinitely, no counting needed. if max_packages == 0: max_packages = None elif arg.startswith('--no-upload'): do_upload = False elif arg == '--version': print(get_client_version()) sys.exit(0) elif arg == '--help': print('Donate CPU to Cppcheck project') print('') print('Syntax: donate-cpu.py [-jN] [--stop-time=HH:MM] [--work-path=path]') print(' -jN Use N threads in compilation/analysis. Default is 1.') print(' --package=url Check a specific package and then stop. Can be useful if you want to reproduce') print(' some warning/crash/exception/etc..') print(' --stop-time=HH:MM Stop analysis when time has passed. Default is that you must terminate the script.') print(' --work-path=path Work folder path. Default path is ' + work_path) print(' --bandwidth-limit=limit Limit download rate for packages. Format for limit is the same that wget uses.') print(' Examples: --bandwidth-limit=250k => max. 250 kilobytes per second') print(' --bandwidth-limit=2m => max. 2 megabytes per second') print(' --max-packages=N Process N packages and then exit. A value of 0 means infinitely.') print(' --no-upload Do not upload anything. Defaults to False.') print(' --packages Process a list of given packages.') print(' --version Returns the version (of the underlying donate_cpu_lib.py).') print('') print('Quick start: just run this script without any arguments') sys.exit(0) else: print('Unhandled argument: ' + arg) sys.exit(1) if sys.version_info.major < 3 or (sys.version_info.major == 3 and sys.version_info.minor < 4): print("#" * 80) print("IMPORTANT") print("Please run the client with at least Python 3.4, thanks!") print("#" * 80) time.sleep(2) sys.exit(1) print('Thank you!') if not check_requirements(): sys.exit(1) if bandwidth_limit and isinstance(bandwidth_limit, str): if subprocess.call(['wget', '--limit-rate=' + bandwidth_limit, '-q', '--spider', 'cppcheck1.osuosl.org']) == 2: print('Error: Bandwidth limit value "' + bandwidth_limit + '" is invalid.') sys.exit(1) else: print('Bandwidth-limit: ' + bandwidth_limit) if package_urls: max_packages = len(package_urls) if max_packages: print('Maximum number of packages to download and analyze: {}'.format(max_packages)) if not os.path.exists(work_path): os.mkdir(work_path) repo_path = os.path.join(work_path, 'repo') # This is a temporary migration step which should be removed in the future migrate_repo_path = os.path.join(work_path, 'cppcheck') packages_processed = 0 print('Get Cppcheck..') try: try_retry(clone_cppcheck, fargs=(repo_path, migrate_repo_path)) except: print('Error: Failed to clone Cppcheck, retry later') sys.exit(1) while True: if max_packages: if packages_processed >= max_packages: print('Processed the specified number of {} package(s). Exiting now.'.format(max_packages)) break print('Processing package {} of the specified {} package(s).'.format(packages_processed + 1, max_packages)) packages_processed += 1 if stop_time: print('stop_time:' + stop_time + '. Time:' + time.strftime('%H:%M') + '.') if stop_time < time.strftime('%H:%M'): print('Stopping. Thank you!') sys.exit(0) cppcheck_versions = get_cppcheck_versions(server_address) if cppcheck_versions is None: print('Failed to communicate with server, retry later') sys.exit(1) if len(cppcheck_versions) == 0: print('Did not get any cppcheck versions from server, retry later') sys.exit(1) for ver in cppcheck_versions: if ver == 'head': ver = 'main' current_cppcheck_dir = os.path.join(work_path, 'tree-'+ver) try: print('Fetching Cppcheck-{}..'.format(ver)) try_retry(checkout_cppcheck_version, fargs=(repo_path, ver, current_cppcheck_dir)) except KeyboardInterrupt as e: # Passthrough for user abort raise e except: print('Failed to update Cppcheck, retry later') sys.exit(1) if ver == 'main': if not compile_cppcheck(current_cppcheck_dir, jobs): print('Failed to compile Cppcheck-{}, retry later'.format(ver)) sys.exit(1) else: if not compile_version(current_cppcheck_dir, jobs): print('Failed to compile Cppcheck-{}, retry later'.format(ver)) sys.exit(1) if package_urls: package = package_urls[packages_processed-1] else: package = get_package(server_address) tgz = download_package(work_path, package, bandwidth_limit) if tgz is None: print("No package downloaded") continue skip_files = None if package.find('/qtcreator/') > 0: # macro_pounder_fn.c is a preprocessor torture test that takes time to finish skip_files = ('macro_pounder_fn.c',) source_path, source_found = unpack_package(work_path, tgz, skip_files=skip_files) if not source_found: print("No files to process") continue crash = False timeout = False count = '' elapsed_time = '' results_to_diff = [] cppcheck_options = '' head_info_msg = '' head_timing_info = '' old_timing_info = '' cppcheck_head_info = '' client_version_head = '' libraries = library_includes.get_libraries(source_path) for ver in cppcheck_versions: tree_path = os.path.join(work_path, 'tree-'+ver) capture_callstack = False if ver == 'head': tree_path = os.path.join(work_path, 'tree-main') cppcheck_head_info = get_cppcheck_info(tree_path) capture_callstack = True def get_client_version_head(): cmd = 'python3' + ' ' + os.path.join(tree_path, 'tools', 'donate-cpu.py') + ' ' + '--version' p = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) try: comm = p.communicate() return comm[0].decode(encoding='utf-8', errors='ignore').strip() except: return None client_version_head = get_client_version_head() c, errout, info, t, cppcheck_options, timing_info = scan_package(tree_path, source_path, jobs, libraries, capture_callstack) if c < 0: if c == -101 and 'error: could not find or open any of the paths given.' in errout: # No sourcefile found (for example only headers present) count += ' 0' elif c == RETURN_CODE_TIMEOUT: # Timeout count += ' TO!' timeout = True else: crash = True count += ' Crash!' else: count += ' ' + str(c) elapsed_time += " {:.1f}".format(t) results_to_diff.append(errout) if ver == 'head': head_info_msg = info head_timing_info = timing_info else: old_timing_info = timing_info output = 'cppcheck-options: ' + cppcheck_options + '\n' output += 'platform: ' + platform.platform() + '\n' output += 'python: ' + platform.python_version() + '\n' output += 'client-version: ' + get_client_version() + '\n' output += 'compiler: ' + get_compiler_version() + '\n' output += 'cppcheck: ' + ' '.join(cppcheck_versions) + '\n' output += 'head-info: ' + cppcheck_head_info + '\n' output += 'count:' + count + '\n' output += 'elapsed-time:' + elapsed_time + '\n' output += 'head-timing-info:\n' + head_timing_info + '\n' output += 'old-timing-info:\n' + old_timing_info + '\n' info_output = output info_output += 'info messages:\n' + head_info_msg if 'head' in cppcheck_versions: output += 'head results:\n' + results_to_diff[cppcheck_versions.index('head')] if not crash and not timeout: output += 'diff:\n' + diff_results(cppcheck_versions[0], results_to_diff[0], cppcheck_versions[1], results_to_diff[1]) + '\n' if package_urls: print('=========================================================') print(output) print('=========================================================') print(info_output) print('=========================================================') if do_upload: if upload_results(package, output, server_address): upload_info(package, info_output, server_address) if not max_packages or packages_processed < max_packages: print('Sleep 5 seconds..') if (client_version_head is not None) and (Version(client_version_head) > Version(get_client_version())): print("ATTENTION: A newer client version ({}) is available - please update!".format(client_version_head)) time.sleep(5)