792 lines
31 KiB
Python
792 lines
31 KiB
Python
# Donate CPU client library
|
|
|
|
import shutil
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import socket
|
|
import time
|
|
import re
|
|
import signal
|
|
import tarfile
|
|
import shlex
|
|
import copy
|
|
|
|
|
|
# Version scheme (MAJOR.MINOR.PATCH) should orientate on "Semantic Versioning" https://semver.org/
|
|
# Every change in this script should result in increasing the version number accordingly (exceptions may be cosmetic
|
|
# changes)
|
|
CLIENT_VERSION = "1.3.51"
|
|
|
|
# Timeout for analysis with Cppcheck in seconds
|
|
CPPCHECK_TIMEOUT = 30 * 60
|
|
|
|
CPPCHECK_REPO_URL = "https://github.com/danmar/cppcheck.git"
|
|
|
|
# Return code that is used to mark a timed out analysis
|
|
RETURN_CODE_TIMEOUT = -999
|
|
|
|
__jobs = '-j1'
|
|
__server_address = ('cppcheck1.osuosl.org', 8000)
|
|
__make_cmd = None
|
|
|
|
def detect_make():
|
|
make_cmds = ['make', 'mingw32-make']
|
|
if sys.platform == 'win32':
|
|
make_cmds.append('msbuild.exe')
|
|
|
|
for m in make_cmds:
|
|
try:
|
|
#print('{} --version'.format(m))
|
|
subprocess.check_call([m, '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
except OSError as e:
|
|
#print("'{}' not found ({})".format(m, e))
|
|
continue
|
|
|
|
print("using '{}'".format(m))
|
|
return m
|
|
|
|
print("Error: a make command ({}) is required".format(','.join(make_cmds)))
|
|
return None
|
|
|
|
|
|
def check_requirements():
|
|
result = True
|
|
|
|
global __make_cmd
|
|
__make_cmd = detect_make()
|
|
if __make_cmd is None:
|
|
result = False
|
|
|
|
apps = ['git', 'wget']
|
|
if __make_cmd in ['make', 'mingw32-make']:
|
|
apps.append('g++')
|
|
apps.append('gdb')
|
|
|
|
for app in apps:
|
|
try:
|
|
#print('{} --version'.format(app))
|
|
subprocess.check_call([app, '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
except OSError:
|
|
print("Error: '{}' is required".format(app))
|
|
result = False
|
|
|
|
try:
|
|
import psutil
|
|
except ImportError as e:
|
|
print("Error: {}. Module is required.".format(e))
|
|
result = False
|
|
|
|
return result
|
|
|
|
|
|
# Try and retry with exponential backoff if an exception is raised
|
|
def try_retry(fun, fargs=(), max_tries=5, sleep_duration=5.0, sleep_factor=2.0):
|
|
for i in range(max_tries):
|
|
try:
|
|
return fun(*fargs)
|
|
except KeyboardInterrupt as e:
|
|
# Do not retry in case of user abort
|
|
raise e
|
|
except BaseException as e:
|
|
if i < max_tries - 1:
|
|
print("{} in {}: {}".format(type(e).__name__, fun.__name__, str(e)))
|
|
print("Trying {} again in {} seconds".format(fun.__name__, sleep_duration))
|
|
time.sleep(sleep_duration)
|
|
sleep_duration *= sleep_factor
|
|
else:
|
|
print("Maximum number of tries reached for {}".format(fun.__name__))
|
|
raise e
|
|
|
|
|
|
def clone_cppcheck(repo_path, migrate_from_path):
|
|
repo_git_dir = os.path.join(repo_path, '.git')
|
|
if os.path.exists(repo_git_dir):
|
|
return
|
|
# Attempt to migrate clone directory used prior to 1.3.17
|
|
if os.path.exists(migrate_from_path):
|
|
os.rename(migrate_from_path, repo_path)
|
|
else:
|
|
# A shallow git clone (depth = 1) is enough for building and scanning.
|
|
# Do not checkout until fetch_cppcheck_version.
|
|
subprocess.check_call(['git', 'clone', '--depth=1', '--no-checkout', CPPCHECK_REPO_URL, repo_path])
|
|
# Checkout an empty branch to allow "git worktree add" for main later on
|
|
try:
|
|
# git >= 2.27
|
|
subprocess.check_call(['git', 'switch', '--orphan', 'empty'], cwd=repo_path)
|
|
except subprocess.CalledProcessError:
|
|
subprocess.check_call(['git', 'checkout', '--orphan', 'empty'], cwd=repo_path)
|
|
|
|
|
|
def checkout_cppcheck_version(repo_path, version, cppcheck_path):
|
|
if not os.path.isabs(cppcheck_path):
|
|
raise ValueError("cppcheck_path is not an absolute path")
|
|
if os.path.exists(cppcheck_path):
|
|
print('Checking out {}'.format(version))
|
|
subprocess.check_call(['git', 'checkout', '-f', version], cwd=cppcheck_path)
|
|
|
|
# It is possible to pull branches, not tags
|
|
if version != 'main':
|
|
return False
|
|
|
|
hash_old = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], cwd=cppcheck_path).strip()
|
|
|
|
print('Pulling {}'.format(version))
|
|
subprocess.check_call(['git', 'pull'], cwd=cppcheck_path)
|
|
|
|
hash_new = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], cwd=cppcheck_path).strip()
|
|
|
|
has_changes = hash_old != hash_new
|
|
if not has_changes:
|
|
print('No changes detected')
|
|
return has_changes
|
|
else:
|
|
if version != 'main':
|
|
print('Fetching {}'.format(version))
|
|
# Since this is a shallow clone, explicitly fetch the remote version tag
|
|
refspec = 'refs/tags/' + version + ':ref/tags/' + version
|
|
subprocess.check_call(['git', 'fetch', '--depth=1', 'origin', refspec], cwd=repo_path)
|
|
print('Adding worktree \'{}\' for {}'.format(cppcheck_path, version))
|
|
subprocess.check_call(['git', 'worktree', 'add', cppcheck_path, version], cwd=repo_path)
|
|
return True
|
|
|
|
|
|
def get_cppcheck_info(cppcheck_path):
|
|
try:
|
|
return subprocess.check_output(['git', 'show', "--pretty=%h (%ci)", 'HEAD', '--no-patch', '--no-notes'], universal_newlines=True, cwd=cppcheck_path).strip()
|
|
except:
|
|
return ''
|
|
|
|
|
|
def __get_cppcheck_binary(cppcheck_path):
|
|
if __make_cmd == "msbuild.exe":
|
|
return os.path.join(cppcheck_path, 'bin', 'cppcheck.exe')
|
|
if __make_cmd == 'mingw32-make':
|
|
return os.path.join(cppcheck_path, 'cppcheck.exe')
|
|
return os.path.join(cppcheck_path, 'cppcheck')
|
|
|
|
|
|
def has_binary(cppcheck_path):
|
|
cppcheck_bin = __get_cppcheck_binary(cppcheck_path)
|
|
if __make_cmd == "msbuild.exe":
|
|
if os.path.isfile(cppcheck_bin):
|
|
return True
|
|
elif __make_cmd == 'mingw32-make':
|
|
if os.path.isfile(cppcheck_bin):
|
|
return True
|
|
elif os.path.isfile(cppcheck_bin):
|
|
return True
|
|
return False
|
|
|
|
|
|
def compile_version(cppcheck_path):
|
|
if has_binary(cppcheck_path):
|
|
return True
|
|
# Build
|
|
ret = compile_cppcheck(cppcheck_path)
|
|
# Clean intermediate build files
|
|
if __make_cmd == "msbuild.exe":
|
|
exclude_bin = 'bin'
|
|
elif __make_cmd == 'mingw32-make':
|
|
exclude_bin = 'cppcheck.exe'
|
|
else:
|
|
exclude_bin = 'cppcheck'
|
|
# TODO: how to support multiple compilers on the same machine? this will clean msbuild.exe files in a mingw32-make build and vice versa
|
|
subprocess.check_call(['git', 'clean', '-f', '-d', '-x', '--exclude', exclude_bin], cwd=cppcheck_path)
|
|
return ret
|
|
|
|
|
|
def compile_cppcheck(cppcheck_path):
|
|
print('Compiling {}'.format(os.path.basename(cppcheck_path)))
|
|
|
|
cppcheck_bin = __get_cppcheck_binary(cppcheck_path)
|
|
# remove file so interrupted "main" branch compilation is being resumed
|
|
if os.path.isfile(cppcheck_bin):
|
|
os.remove(cppcheck_bin)
|
|
|
|
try:
|
|
if __make_cmd == 'msbuild.exe':
|
|
subprocess.check_call(['python3', os.path.join('tools', 'matchcompiler.py'), '--write-dir', 'lib'], cwd=cppcheck_path)
|
|
build_env = os.environ
|
|
# append to cl.exe options - need to omit dash or slash since a dash is being prepended
|
|
build_env["_CL_"] = __jobs.replace('j', 'MP', 1)
|
|
# TODO: processes still exhaust all threads of the system
|
|
subprocess.check_call([__make_cmd, '-t:cli', os.path.join(cppcheck_path, 'cppcheck.sln'), '/property:Configuration=Release;Platform=x64'], cwd=cppcheck_path, env=build_env)
|
|
else:
|
|
build_cmd = [__make_cmd, __jobs, 'MATCHCOMPILER=yes', 'CXXFLAGS=-O2 -g -w']
|
|
build_env = os.environ
|
|
if __make_cmd == 'mingw32-make':
|
|
# TODO: MinGW will always link even if no changes are present
|
|
# assume Python is in PATH for now
|
|
build_env['PYTHON_INTERPRETER'] = 'python3'
|
|
# TODO: MinGW is not detected by Makefile - so work around it for now
|
|
build_cmd.append('RDYNAMIC=-lshlwapi')
|
|
subprocess.check_call(build_cmd, cwd=cppcheck_path, env=build_env)
|
|
except Exception as e:
|
|
print('Compilation failed: {}'.format(e))
|
|
return False
|
|
|
|
try:
|
|
if __make_cmd == 'msbuild.exe':
|
|
subprocess.check_call([os.path.join(cppcheck_path, 'bin', 'cppcheck.exe'), '--version'], cwd=os.path.join(cppcheck_path, 'bin'))
|
|
else:
|
|
subprocess.check_call([os.path.join(cppcheck_path, 'cppcheck'), '--version'], cwd=cppcheck_path)
|
|
except Exception as e:
|
|
print('Running Cppcheck failed: {}'.format(e))
|
|
# remove faulty binary
|
|
if os.path.isfile(cppcheck_bin):
|
|
os.remove(cppcheck_bin)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def get_cppcheck_versions():
|
|
print('Connecting to server to get Cppcheck versions..')
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.connect(__server_address)
|
|
sock.send(b'GetCppcheckVersions\n')
|
|
versions = sock.recv(256)
|
|
# TODO: sock.recv() sometimes hangs and returns b'' afterwards
|
|
if not versions:
|
|
raise Exception('received empty response')
|
|
return versions.decode('utf-8').split()
|
|
|
|
|
|
def get_packages_count():
|
|
def __get_packages_count():
|
|
print('Connecting to server to get count of packages..')
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.connect(__server_address)
|
|
sock.send(b'getPackagesCount\n')
|
|
packages = sock.recv(64)
|
|
# TODO: sock.recv() sometimes hangs and returns b'' afterwards
|
|
if not packages:
|
|
raise Exception('received empty response')
|
|
return int(packages)
|
|
|
|
return try_retry(__get_packages_count)
|
|
|
|
|
|
def get_package(package_index=None):
|
|
def __get_package(package_index):
|
|
print('Connecting to server to get assigned work..')
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.connect(__server_address)
|
|
if package_index is None:
|
|
sock.send(b'get\n')
|
|
else:
|
|
request = 'getPackageIdx:' + str(package_index) + '\n'
|
|
sock.send(request.encode())
|
|
package = sock.recv(256)
|
|
# TODO: sock.recv() sometimes hangs and returns b'' afterwards
|
|
if not package:
|
|
raise Exception('received empty response')
|
|
return package.decode('utf-8')
|
|
|
|
return try_retry(__get_package, fargs=(package_index,), max_tries=3, sleep_duration=30.0, sleep_factor=1.0)
|
|
|
|
|
|
def __handle_remove_readonly(func, path, exc):
|
|
import stat
|
|
if not os.access(path, os.W_OK):
|
|
# Is the error an access error ?
|
|
os.chmod(path, stat.S_IWUSR)
|
|
func(path)
|
|
|
|
|
|
def __remove_tree(folder_name):
|
|
if not os.path.exists(folder_name):
|
|
return
|
|
|
|
def rmtree_func():
|
|
shutil.rmtree(folder_name, onerror=__handle_remove_readonly)
|
|
|
|
print('Removing existing temporary data...')
|
|
try:
|
|
try_retry(rmtree_func, max_tries=5, sleep_duration=30, sleep_factor=1)
|
|
except Exception as e:
|
|
print('Failed to cleanup {}: {}'.format(folder_name, e))
|
|
sys.exit(1)
|
|
|
|
|
|
def __wget(url, destfile, bandwidth_limit):
|
|
if os.path.exists(destfile):
|
|
if os.path.isfile(destfile):
|
|
os.remove(destfile)
|
|
else:
|
|
print('Error: ' + destfile + ' exists but it is not a file! Please check the path and delete it manually.')
|
|
sys.exit(1)
|
|
wget_call = ['wget', '--tries=10', '--timeout=300', '-O', destfile, url]
|
|
if bandwidth_limit and isinstance(bandwidth_limit, str):
|
|
wget_call.append('--limit-rate=' + bandwidth_limit)
|
|
exitcode = subprocess.call(wget_call)
|
|
if exitcode != 0:
|
|
print('wget failed with ' + str(exitcode))
|
|
os.remove(destfile)
|
|
return False
|
|
if not os.path.isfile(destfile):
|
|
return False
|
|
return True
|
|
|
|
|
|
def download_package(work_path, package, bandwidth_limit):
|
|
print('Download package ' + package)
|
|
destfile = os.path.join(work_path, 'temp.tgz')
|
|
if not __wget(package, destfile, bandwidth_limit):
|
|
return None
|
|
return destfile
|
|
|
|
|
|
def unpack_package(work_path, tgz, cpp_only=False, c_only=False, skip_files=None):
|
|
temp_path = os.path.join(work_path, 'temp')
|
|
__remove_tree(temp_path)
|
|
os.mkdir(temp_path)
|
|
|
|
header_endings = ('.hpp', '.h++', '.hxx', '.hh', '.h')
|
|
cpp_source_endings = ('.cpp', '.c++', '.cxx', '.cc', '.tpp', '.txx', '.ipp', '.ixx', '.qml')
|
|
c_source_endings = ('.c',)
|
|
|
|
if cpp_only:
|
|
source_endings = cpp_source_endings
|
|
elif c_only:
|
|
source_endings = c_source_endings
|
|
else:
|
|
source_endings = cpp_source_endings + c_source_endings
|
|
|
|
source_found = False
|
|
if tarfile.is_tarfile(tgz):
|
|
print('Unpacking..')
|
|
with tarfile.open(tgz) as tf:
|
|
total = 0
|
|
extracted = 0
|
|
skipped = 0
|
|
for member in tf:
|
|
total += 1
|
|
if member.name.startswith(('/', '..')):
|
|
# Skip dangerous file names
|
|
# print('skipping dangerous file: ' + member.name)
|
|
skipped += 1
|
|
continue
|
|
|
|
is_source = member.name.lower().endswith(source_endings)
|
|
if is_source or member.name.lower().endswith(header_endings):
|
|
if skip_files is not None:
|
|
skip = False
|
|
for skip_file in skip_files:
|
|
if member.name.endswith('/' + skip_file):
|
|
# print('found file to skip: ' + member.name)
|
|
skip = True
|
|
break
|
|
if skip:
|
|
skipped += 1
|
|
continue
|
|
try:
|
|
tf.extract(member.name, temp_path)
|
|
if is_source:
|
|
source_found = True
|
|
extracted += 1
|
|
except OSError:
|
|
pass
|
|
except AttributeError:
|
|
pass
|
|
print('extracted {} of {} files (skipped {}{})'.format(extracted, total, skipped, (' / only headers' if (extracted and not source_found) else '')))
|
|
return temp_path, source_found
|
|
|
|
|
|
def __run_command(cmd, print_cmd=True):
|
|
if print_cmd:
|
|
print(cmd)
|
|
time_start = time.time()
|
|
comm = None
|
|
if sys.platform == 'win32':
|
|
p = subprocess.Popen(shlex.split(cmd, comments=False, posix=False), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, errors='surrogateescape')
|
|
else:
|
|
p = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, errors='surrogateescape', preexec_fn=os.setsid)
|
|
try:
|
|
comm = p.communicate(timeout=CPPCHECK_TIMEOUT)
|
|
return_code = p.returncode
|
|
p = None
|
|
except subprocess.TimeoutExpired:
|
|
import psutil
|
|
return_code = RETURN_CODE_TIMEOUT
|
|
# terminate all the child processes so we get messages about which files were hanging
|
|
child_procs = psutil.Process(p.pid).children(recursive=True)
|
|
if len(child_procs) > 0:
|
|
for child in child_procs:
|
|
child.terminate()
|
|
try:
|
|
# call with timeout since it might get stuck e.g. gcc-arm-none-eabi
|
|
comm = p.communicate(timeout=5)
|
|
p = None
|
|
except subprocess.TimeoutExpired:
|
|
pass
|
|
finally:
|
|
if p:
|
|
os.killpg(os.getpgid(p.pid), signal.SIGTERM) # Send the signal to all the process groups
|
|
comm = p.communicate()
|
|
time_stop = time.time()
|
|
stdout, stderr = comm
|
|
elapsed_time = time_stop - time_start
|
|
return return_code, stdout, stderr, elapsed_time
|
|
|
|
|
|
def scan_package(cppcheck_path, source_path, libraries, capture_callstack=True):
|
|
print('Analyze..')
|
|
libs = ''
|
|
for library in libraries:
|
|
if os.path.exists(os.path.join(cppcheck_path, 'cfg', library + '.cfg')):
|
|
libs += '--library=' + library + ' '
|
|
|
|
dir_to_scan = source_path
|
|
|
|
# TODO: temporarily disabled timing information - use --showtime=top5_summary when next version is released
|
|
# TODO: remove missingInclude disabling when it no longer is implied by --enable=information
|
|
# Reference for GNU C: https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html
|
|
options = libs + ' --check-library --inconclusive --enable=style,information --inline-suppr --disable=missingInclude --suppress=unmatchedSuppression --template=daca2'
|
|
options += ' --debug-warnings --suppress=autoNoType --suppress=valueFlowBailout --suppress=bailoutUninitVar --suppress=symbolDatabaseWarning'
|
|
options += ' -D__GNUC__ --platform=unix64'
|
|
options_rp = options + ' -rp={}'.format(dir_to_scan)
|
|
if __make_cmd == 'msbuild.exe':
|
|
cppcheck_cmd = os.path.join(cppcheck_path, 'bin', 'cppcheck.exe') + ' ' + options_rp
|
|
cmd = cppcheck_cmd + ' ' + __jobs + ' ' + dir_to_scan
|
|
else:
|
|
nice_cmd = 'nice'
|
|
if __make_cmd == 'mingw32-make':
|
|
nice_cmd = ''
|
|
cppcheck_cmd = os.path.join(cppcheck_path, 'cppcheck') + ' ' + options_rp
|
|
cmd = nice_cmd + ' ' + cppcheck_cmd + ' ' + __jobs + ' ' + dir_to_scan
|
|
returncode, stdout, stderr, elapsed_time = __run_command(cmd)
|
|
|
|
# collect messages
|
|
information_messages_list = []
|
|
issue_messages_list = []
|
|
internal_error_messages_list = []
|
|
count = 0
|
|
re_obj = None
|
|
for line in stderr.split('\n'):
|
|
if ': information: ' in line:
|
|
information_messages_list.append(line + '\n')
|
|
elif line:
|
|
issue_messages_list.append(line + '\n')
|
|
if re_obj is None:
|
|
re_obj = re.compile(r'.*:[0-9]+:.*\]$')
|
|
if re_obj.match(line):
|
|
count += 1
|
|
if ': error: Internal error: ' in line:
|
|
internal_error_messages_list.append(line + '\n')
|
|
print('Number of issues: ' + str(count))
|
|
# Collect timing information
|
|
stdout_lines = stdout.split('\n')
|
|
timing_info_list = []
|
|
overall_time_found = False
|
|
max_timing_lines = 6
|
|
current_timing_lines = 0
|
|
for reverse_line in reversed(stdout_lines):
|
|
if reverse_line.startswith('Overall time:'):
|
|
overall_time_found = True
|
|
if overall_time_found:
|
|
if not reverse_line or current_timing_lines >= max_timing_lines:
|
|
break
|
|
timing_info_list.insert(0, ' ' + reverse_line + '\n')
|
|
current_timing_lines += 1
|
|
timing_str = ''.join(timing_info_list)
|
|
|
|
# detect errors
|
|
sig_file = None
|
|
sig_num = -1
|
|
for ie_line in internal_error_messages_list:
|
|
# temp/dlib-19.10/dlib/test/dnn.cpp:0:0: error: Internal error: Child process crashed with signal 11 [cppcheckError]
|
|
if 'Child process crashed with signal' in ie_line:
|
|
sig_file = ie_line.split(':')[0]
|
|
sig_msg = 'signal '
|
|
sig_pos = ie_line.find(sig_msg)
|
|
if sig_pos != -1:
|
|
sig_start_pos = sig_pos + len(sig_msg)
|
|
sig_num = int(ie_line[sig_start_pos:ie_line.find(' ', sig_start_pos)])
|
|
# break on the first signalled file for now
|
|
break
|
|
print('cppcheck finished with ' + str(returncode) + ('' if sig_num == -1 else ' (signal ' + str(sig_num) + ')') + ' in {:.1f}s'.format(elapsed_time))
|
|
|
|
options_j = options + ' ' + __jobs
|
|
|
|
if returncode == RETURN_CODE_TIMEOUT:
|
|
print('Timeout!')
|
|
return returncode, ''.join(internal_error_messages_list), '', elapsed_time, options_j, ''
|
|
|
|
# generate stack trace for SIGSEGV, SIGABRT, SIGILL, SIGFPE, SIGBUS
|
|
has_error = returncode in (-11, -6, -4, -8, -7)
|
|
has_sig = sig_num in (11, 6, 4, 8, 7)
|
|
if has_error or has_sig:
|
|
print('Crash!')
|
|
# make sure we have the actual error code set
|
|
if has_sig:
|
|
returncode = -sig_num
|
|
stacktrace = ''
|
|
if capture_callstack:
|
|
# re-run within gdb to get a stacktrace
|
|
cmd = 'gdb --batch --eval-command=run --eval-command="bt 50" --return-child-result --args ' + cppcheck_cmd + " -j1 "
|
|
if sig_file is not None:
|
|
cmd += sig_file
|
|
else:
|
|
cmd += dir_to_scan
|
|
_, st_stdout, _, _ = __run_command(cmd)
|
|
gdb_pos = st_stdout.find(" received signal")
|
|
if not gdb_pos == -1:
|
|
last_check_pos = st_stdout.rfind('Checking ', 0, gdb_pos)
|
|
if last_check_pos == -1:
|
|
stacktrace = st_stdout[gdb_pos:]
|
|
else:
|
|
stacktrace = st_stdout[last_check_pos:]
|
|
# if no stacktrace was generated return the original stdout or internal errors list
|
|
if not stacktrace:
|
|
if has_sig:
|
|
stacktrace = ''.join(internal_error_messages_list)
|
|
else:
|
|
stacktrace = stdout
|
|
return returncode, stacktrace, '', returncode, options_j, ''
|
|
|
|
if returncode != 0:
|
|
# returncode is always 1 when this message is written
|
|
thr_pos = stderr.find('#### ThreadExecutor')
|
|
if thr_pos != -1:
|
|
print('Thread!')
|
|
return -222, stderr[thr_pos:], '', -222, options_j, ''
|
|
|
|
print('Error!')
|
|
if returncode > 0:
|
|
returncode = -100-returncode
|
|
return returncode, stdout, '', returncode, options_j, ''
|
|
|
|
if sig_num != -1:
|
|
print('Signal!')
|
|
return -sig_num, ''.join(internal_error_messages_list), '', -sig_num, options_j, ''
|
|
|
|
return count, ''.join(issue_messages_list), ''.join(information_messages_list), elapsed_time, options_j, timing_str
|
|
|
|
|
|
def __split_results(results):
|
|
ret = []
|
|
w = None
|
|
re_obj = None
|
|
for line in results.split('\n'):
|
|
if line.endswith(']'):
|
|
if re_obj is None:
|
|
re_obj = re.compile(r': (error|warning|style|performance|portability|information|debug):')
|
|
if re_obj.search(line):
|
|
if w is not None:
|
|
ret.append(w.strip())
|
|
w = ''
|
|
if w is not None:
|
|
w += ' ' * 5 + line + '\n'
|
|
if w is not None:
|
|
ret.append(w.strip())
|
|
return ret
|
|
|
|
|
|
def diff_results(ver1, results1, ver2, results2):
|
|
print('Diff results..')
|
|
ret = ''
|
|
r1 = sorted(__split_results(results1))
|
|
r2 = sorted(__split_results(results2))
|
|
i1 = 0
|
|
i2 = 0
|
|
while i1 < len(r1) and i2 < len(r2):
|
|
if r1[i1] == r2[i2]:
|
|
i1 += 1
|
|
i2 += 1
|
|
elif r1[i1] < r2[i2]:
|
|
ret += ver1 + ' ' + r1[i1] + '\n'
|
|
i1 += 1
|
|
else:
|
|
ret += ver2 + ' ' + r2[i2] + '\n'
|
|
i2 += 1
|
|
while i1 < len(r1):
|
|
ret += ver1 + ' ' + r1[i1] + '\n'
|
|
i1 += 1
|
|
while i2 < len(r2):
|
|
ret += ver2 + ' ' + r2[i2] + '\n'
|
|
i2 += 1
|
|
|
|
# if there are syntaxError/unknownMacro/etc then analysis stops.
|
|
# diffing normal checker warnings will not make much sense
|
|
bailout_ids = ('[syntaxError]', '[unknownMacro]')
|
|
has_bailout_id = False
|
|
for id in bailout_ids:
|
|
if (id in results1) or (id in results1):
|
|
has_bailout_id = True
|
|
if has_bailout_id:
|
|
def check_bailout(line):
|
|
for id in bailout_ids:
|
|
if line.endswith(id):
|
|
return True
|
|
return False
|
|
out = ''
|
|
for line in ret.split('\n'):
|
|
if check_bailout(line):
|
|
out += line + '\n'
|
|
ret = out
|
|
|
|
return ret
|
|
|
|
|
|
def __send_all(connection, data):
|
|
bytes_ = data.encode('ascii', 'ignore')
|
|
while bytes_:
|
|
num = connection.send(bytes_)
|
|
if num < len(bytes_):
|
|
bytes_ = bytes_[num:]
|
|
else:
|
|
bytes_ = None
|
|
|
|
|
|
def __upload(cmd, data, cmd_info):
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.connect(__server_address)
|
|
__send_all(sock, '{}\n{}'.format(cmd, data))
|
|
print('{} has been successfully uploaded.'.format(cmd_info))
|
|
return True
|
|
|
|
|
|
def upload_results(package, results):
|
|
if not __make_cmd == 'make':
|
|
print('Error: Result upload not performed - only make build binaries are currently fully supported')
|
|
return False
|
|
|
|
print('Uploading results.. ' + str(len(results)) + ' bytes')
|
|
try:
|
|
try_retry(__upload, fargs=('write\n' + package, results + '\nDONE', 'Result'), max_tries=20, sleep_duration=15, sleep_factor=1)
|
|
except Exception as e:
|
|
print('Result upload failed ({})!'.format(e))
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def upload_info(package, info_output):
|
|
if not __make_cmd == 'make':
|
|
print('Error: Information upload not performed - only make build binaries are currently fully supported')
|
|
return False
|
|
|
|
print('Uploading information output.. ' + str(len(info_output)) + ' bytes')
|
|
try:
|
|
try_retry(__upload, fargs=('write_info\n' + package, info_output + '\nDONE', 'Information'), max_tries=20, sleep_duration=15, sleep_factor=1)
|
|
except Exception as e:
|
|
print('Information upload failed ({})!'.format(e))
|
|
return False
|
|
|
|
return True
|
|
|
|
def upload_nodata(package):
|
|
print('Uploading no-data status..')
|
|
try:
|
|
try_retry(__upload, fargs=('write_nodata\n' + package, '', 'No-data status'), max_tries=3, sleep_duration=30, sleep_factor=1)
|
|
except Exception as e:
|
|
print('No-data upload failed ({})!'.format(e))
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
class LibraryIncludes:
|
|
def __init__(self):
|
|
include_mappings = {'boost': ['<boost/'],
|
|
'bsd': ['<sys/queue.h>', '<sys/tree.h>', '<sys/uio.h>','<bsd/', '<fts.h>', '<db.h>', '<err.h>', '<vis.h>'],
|
|
'cairo': ['<cairo.h>'],
|
|
'cppunit': ['<cppunit/'],
|
|
'icu': ['<unicode/', '"unicode/'],
|
|
'ginac': ['<ginac/', '"ginac/'],
|
|
'googletest': ['<gtest/gtest.h>'],
|
|
'gtk': ['<gtk', '<glib.h>', '<glib-', '<glib/', '<gnome'],
|
|
'kde': ['<KGlobal>', '<KApplication>', '<KDE/'],
|
|
'libcerror': ['<libcerror.h>'],
|
|
'libcurl': ['<curl/curl.h>'],
|
|
'libsigc++': ['<sigc++/'],
|
|
'lua': ['<lua.h>', '"lua.h"'],
|
|
'mfc': ['<afx.h>', '<afxwin.h>', '<afxext.h>'],
|
|
'microsoft_atl': ['<atlbase.h>'],
|
|
'microsoft_sal': ['<sal.h>'],
|
|
'motif': ['<X11/', '<Xm/'],
|
|
'nspr': ['<prtypes.h>', '"prtypes.h"'],
|
|
'ntl': ['<ntl/', '"ntl/'],
|
|
'opencv2': ['<opencv2/', '"opencv2/'],
|
|
'opengl': ['<GL/gl.h>', '<GL/glu.h>', '<GL/glut.h>'],
|
|
'openmp': ['<omp.h>'],
|
|
'openssl': ['<openssl/'],
|
|
'pcre': ['<pcre.h>', '"pcre.h"'],
|
|
'python': ['<Python.h>', '"Python.h"'],
|
|
'qt': ['<QAbstractSeries>', '<QAction>', '<QActionGroup>', '<QApplication>', '<QByteArray>', '<QChartView>', '<QClipboard>', '<QCloseEvent>', '<QColor>', '<QColorDialog>', '<QComboBox>', '<QCoreApplication>', '<QCryptographicHash>', '<QDate>', '<QDateTime>', '<QDateTimeAxis>', '<QDebug>', '<QDesktopServices>', '<QDialog>', '<QDialogButtonBox>', '<QDir>', '<QElapsedTimer>', '<QFile>', '<QFileDialog>', '<QFileInfo>', '<QFileInfoList>', '<QFlags>', '<QFont>', '<QFormLayout>', '<QHelpContentWidget>', '<QHelpEngine>', '<QHelpIndexWidget>', '<QImageReader>', '<QInputDialog>', '<QKeyEvent>', '<QLabel>', '<QLineSeries>', '<QList>', '<qlist.h>', '<QLocale>', '<QMainWindow>', '<QMap>', '<QMenu>', '<QMessageBox>', '<QMetaType>', '<QMimeData>', '<QMimeDatabase>', '<QMimeType>', '<QMutex>', '<QObject>', '<qobjectdefs.h>', '<QPainter>', '<QPlainTextEdit>', '<QPrintDialog>', '<QPrinter>', '<QPrintPreviewDialog>', '<QProcess>', '<QPushButton>', '<QQueue>', '<QReadWriteLock>', '<QRegularExpression>', '<QRegularExpressionValidator>', '<QSet>', '<QSettings>', '<QShortcut>', '<QSignalMapper>', '<QStandardItemModel>', '<QString>', '<qstring.h>', '<QStringList>', '<QSyntaxHighlighter>', '<QTest>', '<QTextBrowser>', '<QTextDocument>', '<QTextEdit>', '<QTextStream>', '<QThread>', '<QTimer>', '<QTranslator>', '<QTreeView>', '<QtWidgets>', '<QUrl>', '<QValueAxis>', '<QVariant>', '<QWaitCondition>', '<QWidget>', '<QXmlStreamReader>', '<QXmlStreamWriter>', '<QtGui'],
|
|
'ruby': ['<ruby.h>', '<ruby/', '"ruby.h"'],
|
|
'sdl': ['<SDL.h>', '<SDL/SDL.h>', '<SDL2/SDL.h>'],
|
|
'sqlite3': ['<sqlite3.h>', '"sqlite3.h"'],
|
|
'tinyxml2': ['<tinyxml2', '"tinyxml2'],
|
|
'wxsqlite3': ['<wx/wxsqlite3', '"wx/wxsqlite3'],
|
|
'wxwidgets': ['<wx/', '"wx/'],
|
|
'zlib': ['<zlib.h>'],
|
|
}
|
|
|
|
self.__library_includes_re = {}
|
|
|
|
for library, includes in include_mappings.items():
|
|
re_includes = [re.escape(inc) for inc in includes]
|
|
re_expr = '^[ \\t]*#[ \\t]*include[ \\t]*(?:' + '|'.join(re_includes) + ')'
|
|
re_obj = re.compile(re_expr, re.MULTILINE)
|
|
self.__library_includes_re[library] = re_obj
|
|
|
|
def __iterate_files(self, path, has_include_cb):
|
|
for root, _, files in os.walk(path):
|
|
for name in files:
|
|
filename = os.path.join(root, name)
|
|
try:
|
|
with open(filename, 'rt', errors='ignore') as f:
|
|
filedata = f.read()
|
|
has_include_cb(filedata)
|
|
except IOError:
|
|
pass
|
|
|
|
def get_libraries(self, folder):
|
|
print('Detecting library usage...')
|
|
libraries = ['posix', 'gnu']
|
|
|
|
# explicitly copy as assignments in python are references
|
|
library_includes_re = copy.copy(self.__library_includes_re)
|
|
|
|
def has_include(filedata):
|
|
lib_del = []
|
|
for library, includes_re in library_includes_re.items():
|
|
if includes_re.search(filedata):
|
|
libraries.append(library)
|
|
lib_del.append(library)
|
|
|
|
for lib_d in lib_del:
|
|
del library_includes_re[lib_d]
|
|
|
|
self.__iterate_files(folder, has_include)
|
|
print('Found libraries: {}'.format(libraries))
|
|
return libraries
|
|
|
|
|
|
def get_compiler_version():
|
|
if __make_cmd == 'msbuild.exe':
|
|
_, _, stderr, _ = __run_command('cl.exe', False)
|
|
return stderr.split('\n')[0]
|
|
|
|
_, stdout, _, _ = __run_command('g++ --version', False)
|
|
return stdout.split('\n')[0]
|
|
|
|
|
|
def get_client_version():
|
|
return CLIENT_VERSION
|
|
|
|
|
|
def set_server_address(server_address):
|
|
global __server_address
|
|
__server_address = server_address
|
|
|
|
|
|
def set_jobs(jobs: str):
|
|
global __jobs
|
|
__jobs = jobs
|
|
|
|
library_includes = LibraryIncludes()
|