#!/usr/bin/python # -*- coding: utf-8 -*- import argparse import errno import os import subprocess import sys import tempfile def get_gradle_command_and_project_mtime(dir): """Find top-project gradlew or fallback to using gradle on sub-project dir. While iterating the paths, also check mtime on build.gradle files""" topdir = dir build_gradle = None gradlew = None mtime = -1 while True: test = os.path.join(topdir, 'build.gradle') if os.path.exists(test): if not build_gradle: build_gradle = test try: m = os.path.getmtime(test) if m > mtime: mtime = m except OSError: pass test = os.path.join(topdir, 'gradlew') if os.path.exists(test): gradlew = test break test = os.path.dirname(topdir) if test == topdir: break topdir = test if gradlew: cmd = [gradlew, '-p', topdir] elif build_gradle: topdir = os.path.dirname(build_gradle) cmd = ['gradle', '-p', topdir] else: topdir = '' cmd = ['gradle'] return (cmd, mtime, topdir) def filter_source_files(files): """Remove any item not looking like a kotlin or java source file.""" return [f for f in files if f.endswith('.kt') or f.endswith('.java')] def cleanup_output(tmp, real): """Remove any file in tmp that is older than the one in real""" offset = len(tmp) if tmp[-1] != '/': offset = offset + 1 for (dirpath, _, files) in os.walk(tmp): realpath = os.path.join(real, dirpath[offset:]) for filename in files: try: mtime = os.path.getmtime(os.path.join(realpath, filename)) tmp = os.path.join(dirpath, filename) if os.path.getmtime(tmp) <= mtime: os.remove(tmp) except OSError: pass def run_kotlinc(cp, files, output, args, sourcefile, outdir): """Execute kotlinc with the given options.""" command = ['kotlinc', '-J-Xms1g', '-J-Xmx3g'] if outdir: command.extend(['-d', outdir]) tmp = [outdir] if output: tmp.append(output) if cp: tmp.extend(cp) if tmp: command.extend(['-cp', ':'.join(tmp)]) if os.fork() == 0: cleanup_output(outdir, output) sys.exit(0) else: if cp: command.extend(['-cp', ':'.join(cp)]) if output: command.extend(['-d', output]) command.extend([arg for arg in args if not arg.startswith('-Xplugin=')]) # Save time? command.append('-Xno-optimize') command.append(sourcefile) # Must include all source files or internal access will fail if outdir: with tempfile.NamedTemporaryFile(mode='w') as f: command.append('@' + f.name) for source in filter_source_files(files): f.write(source) f.write('\n') f.flush() return subprocess.call(command) else: with tempfile.NamedTemporaryFile(mode='w') as f: command.append('@' + f.name) for source in filter_source_files(files): f.write(source) f.write('\n') f.flush() return subprocess.call(command) def file_in_list(needle, files): """Find needle in files and if so, return the list without needle. Otherwise return None.""" if os.path.exists(needle): for i in range(0, len(files) - 1): if os.path.exists(files[i]) and os.path.samefile(needle, files[i]): return files[0:i] + files[i + 1:] return None def split_variant(variant): ret = [] last = 0 for i in range(0, len(variant)): if variant[i:i+1].isupper(): ret.append(variant[last:i]) last = i ret.append(variant[last:]) return ret def match_variant(variant1, variant2): v1 = split_variant(variant1) v2 = split_variant(variant2) return all(x in v1 for x in v2) or all(x in v2 for x in v1) def figure_out_kotlin_compilation(sessiondir, sourcefile, tempfile, detekt, variant, run_gen, force=False): """Get options for Kotlin compilation from gradle project and run kotlinc.""" (cmd, mtime, projectdir) = get_gradle_command_and_project_mtime( os.path.dirname(sourcefile)) output = None cached = False if sessiondir != None: outdir = os.path.join(sessiondir, 'kotlin_output') try: os.mkdir(outdir) except OSError as exc: if exc.errno != errno.EEXIST or not os.path.isdir(outdir): outdir = None pass cachefile = os.path.join(sessiondir, 'gradle_kotlin_output') try: if not force and os.path.getmtime(cachefile) >= mtime: with open(cachefile, 'r') as f: output = f.read() cached = True except (OSError, IOError): pass else: outdir = None if not output: flycheck_cmd = cmd + ['-q', 'flycheckAndroidKotlin'] output = subprocess.check_output(flycheck_cmd, universal_newlines=True) if sessiondir != None: try: with open(cachefile, 'w') as f: f.write(output) except IOError: pass output = output.split('\n') data = None data_type = None variants = [] gen = [] ret = 0 compiled = False for line in output: if line == '***' or line == '!!!': if data_type == '*': variants.append({'data': data, 'gen': len(gen)}) elif data_type == '!' and variants: gen.append(data) data_type = line[0] if data_type == '*': data = {} else: data = [] elif data_type == '*': if line.startswith('variant='): data['variant'] = line[8:] if line.startswith('args='): data['args'] = line[6:-1].split(', ') elif line.startswith('cp='): data['cp'] = line[3:].split(':') elif line.startswith('files='): data['files'] = line[6:].split(':') elif line.startswith('output='): data['output'] = line[7:] elif data_type == '!': data.append(line) if data_type == '*': variants.append({'data': data, 'gen': len(gen)}) elif data_type == '!' and variants: gen.append(data) fallback = None if variant: for v in variants: data = v['data'] if 'variant' in data and match_variant(data['variant'], variant): if not fallback: fallback = data files = file_in_list(sourcefile, data['files']) if files: if run_gen and v['gen'] < len(gen): subprocess.call(cmd + ['-q'] + gen[v['gen']]) ret = run_kotlinc(data['cp'], files, data['output'], data['args'], tempfile, outdir) compiled = True break if not compiled and variants: data = variants[0]['data'] first = data['variant'] if 'variant' in data else None for v in variants: data = v['data'] if first and 'variant' in data and data['variant'] == first: if not fallback: fallback = data files = file_in_list(sourcefile, data['files']) if files: if run_gen and v['gen'] < len(gen): subprocess.call(cmd + ['-q'] + gen[v['gen']]) ret = run_kotlinc(data['cp'], files, data['output'], data['args'], tempfile, outdir) compiled = True break if not compiled: # Probably need to rerun gradle to find group for sourcefile if cached and os.path.exists(sourcefile): return figure_out_kotlin_compilation(sessiondir, sourcefile, tempfile, detekt, variant, run_gen, force=True) # OK, perhaps file doesn't exist yet or not yet added to gradle, # whatever, assume the first group is good enough if fallback: ret = run_kotlinc(fallback['cp'], fallback['files'], fallback['output'], fallback['args'], tempfile, outdir) compiled = True if not ret and detekt: cmd = [detekt['cli']] if 'config' in detekt: if detekt['config']: cmd.extend(['-c', detekt['config']]) else: cmd.append('--build-upon-default-config') cmd.append('-i') cmd.append(tempfile) if len(projectdir): cwd = projectdir else: cwd = None ret = subprocess.call(cmd, cwd=cwd) if not ret and not compiled: print("Source file not in project and project seems empty!") ret = -1 return ret def main(argv): parser = argparse.ArgumentParser() parser.add_argument('--detekt-cli') parser.add_argument('--detekt-config') parser.add_argument('--variant') parser.add_argument('--skip-gen', dest='gen', action='store_const', const=False, default=True) parser.add_argument('sessiondir', nargs='?') parser.add_argument('sourcefile') parser.add_argument('tempfile', nargs='?') args = parser.parse_args(argv[1:]) sourcefile = args.sourcefile tempfile = args.tempfile or sourcefile detekt = None if args.detekt_cli: detekt = {'cli': args.detekt_cli, 'config': args.detekt_config} sourcefile = os.path.abspath(sourcefile) return figure_out_kotlin_compilation(args.sessiondir, sourcefile, tempfile, detekt, args.variant, args.gen) sys.exit(main(sys.argv))