#!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 # # Copyright (c) 2014, 2015, 2016 Samsung Electronics.Co.Ltd. # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free # Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. # import sys import os import subprocess import re import argparse from argparse import ArgumentParser import ConfigParser import glob import fnmatch import shutil import zipfile import errno g_home = os.path.dirname(os.path.realpath(__file__)) class LocalError(Exception): """Local error exception.""" pass class Executor(object): """Subprocess wrapper""" def __init__(self, checker=None): self.stdout = subprocess.PIPE self.stderr = subprocess.STDOUT self.checker = checker def run(self, cmdline_or_args, show=False, checker=False): """Execute external command""" out = '' try: process = subprocess.Popen(cmdline_or_args, \ stdout=self.stdout, \ stderr=self.stderr, \ shell=True) while True: line = process.stdout.readline() if show: print line.rstrip() out = out + '\n' + line.rstrip() if not line: break except: raise LocalError('Running process failed') return out def list_files(path, ext=None): f_list = [] for root, dirnames, filenames in os.walk(path): if ext is None: for filename in filenames: f_list.append(os.path.join(root, filename)) else: for filename in fnmatch.filter(filenames, '*.'+ext): f_list.append(os.path.join(root, filename)) return f_list class FakeSecHead(object): def __init__(self, fp): self.fp = fp self.sechead = '[ascection]\n' def readline(self): if self.sechead: try: return self.sechead finally: self.sechead = None else: return self.fp.readline() class ErrorParser(object): """Inspect specific error string""" parsers = [] def __init__(self): ErrorParser = {'GNU_LINKER':['(.*?):?\(\.\w+\+.*\): (.*)', \ '(.*[/\\\])?ld(\.exe)?: (.*)'], \ 'GNU_GCC':['(.*?):(\d+):(\d+:)? [Ee]rror: ([`\'"](.*)[\'"] undeclared .*)', \ '(.*?):(\d+):(\d+:)? [Ee]rror: (conflicting types for .*[`\'"](.*)[\'"].*)', \ '(.*?):(\d+):(\d+:)? (parse error before.*[`\'"](.*)[\'"].*)', \ '(.*?):(\d+):(\d+:)?\s*(([Ee]rror)|(ERROR)): (.*)'], \ # '(.*?):(\d+):(\d+:)? (.*)'], \ 'GNU_GMAKE':['(.*):(\d*): (\*\*\* .*)', \ '.*make.*: \*\*\* .*', \ '.*make.*: Target (.*) not remade because of errors.', \ '.*[Cc]ommand not found.*', \ 'Error:\s*(.*)'], \ 'TIZEN_NATIVE':['.*ninja: build stopped.*', \ 'edje_cc: Error..(.*):(\d).*', \ 'edje_cc: Error.*']} for parser in ErrorParser: parser_env = os.getenv('SDK_ERROR_'+parser) if parser_env: self.parsers.append(parser_env) else: for msg in ErrorParser[parser]: self.parsers.append(msg) def check(self, full_log): """Check error string line by line""" #snipset_text = full_log[:full_log.rfind('PLATFORM_VER\t')].split('\n') for line in full_log.split('\n'): errline = re.search('|'.join(self.parsers), line[:1024]) if errline: return errline.string #Errors return None #No error class _Rootstrap(object): """Tizen SDK rootstrap info. Used only in Sdk class""" rootstrap_list = None sdk_path = None def __init__(self, sdk_path=None, config=None): self.tizen = sdk_path self.list_rootstrap() self.config_file = config def list_rootstrap(self): """List all the rootstraps""" if self.rootstrap_list != None: return self.rootstrap_list cmdline = self.tizen + ' list rootstrap' ret = Executor().run(cmdline, show=False) for x in ret.splitlines(): if re.search('(mobile|wearable)-(2.4|3.0)-(device|emulator|device64|emulator64).core.*', x): if self.rootstrap_list == None: self.rootstrap_list = [] self.rootstrap_list.append(x.split(' ')[0]) return self.rootstrap_list def check_rootstrap(self, rootstrap, show=True): """Specific rootstrap is in the SDK Otherwise use default""" if rootstrap == None: rootstrap = 'mobile-3.0-emulator.core' #default if rootstrap in self.rootstrap_list: return rootstrap else: if show == True: print ' ERROR: Rootstrap [%s] does not exist' % rootstrap print ' Update your rootstrap or use one of:\n %s' \ % '\n '.join(self.list_rootstrap()) return None class Sdk(object): """Tizen SDK related job""" rs = None #_Rootstrap class instance rootstrap_list = None sdk_to_search = ['tizen-sdk/tools/ide/bin/tizen', \ 'tizen-sdk-ux/tools/ide/bin/tizen', \ 'tizen-sdk-cli/tools/ide/bin/tizen'] def __init__(self, sdkpath=None): self.error_parser = ErrorParser() self.runtool = Executor(checker=self.error_parser) self.home = os.getenv('HOME') self.config_file = os.path.join(g_home, '.abs') if sdkpath is None: self.tizen = self.get_user_root() if self.tizen is None or self.tizen == '': for i in self.sdk_to_search: if os.path.isfile(os.path.join(self.home, i)): self.tizen = os.path.join(self.home, i) break else: self.tizen = os.path.join(sdkpath, 'tools/ide/bin/tizen') self.update_user_root(self.tizen) if not os.path.isfile(self.tizen): print 'Cannot locate cli tool' raise LocalError('Fail to locate cli tool') self.rs = _Rootstrap(sdk_path=self.tizen, config=self.config_file) def get_user_root(self): if os.path.isfile(self.config_file): config = ConfigParser.RawConfigParser() config.read(self.config_file) return config.get('Global', 'tizen') return None def update_user_root(self, path): if not os.path.isfile(self.config_file): with open(self.config_file, 'w') as f: f.write('[Global]\n') f.write('tizen = %s\n' % path) return config = ConfigParser.RawConfigParser() config.read(self.config_file) config.set('Global', 'tizen', path) with open(self.config_file, 'wb') as cf: config.write(cf) def list_rootstrap(self): return self.rs.list_rootstrap() def check_rootstrap(self, rootstrap): return self.rs.check_rootstrap(rootstrap) def _run(self, command, args, show=True, checker=False): """Run a tizen command""" cmd = [self.tizen, command] + args print '\nRunning command:\n %s' % ' '.join(cmd) return self.runtool.run(' '.join(cmd), show=show, checker=checker) def copytree2(self, src, dst, symlinks=False, ignore=None): """Copy with Ignore & Overwrite""" names = os.listdir(src) if ignore is not None: ignored_names = ignore(src, names) else: ignored_names = set() try: os.makedirs(dst) except: pass errors = [] for name in names: if name in ignored_names: continue srcname = os.path.join(src, name) dstname = os.path.join(dst, name) try: if symlinks and os.path.islink(srcname): linkto = os.readlink(srcname) os.symlink(linkto, dstname) elif os.path.isdir(srcname): self.copytree2(srcname, dstname, symlinks, ignore) else: # Will raise a SpecialFileError for unsupported file types shutil.copy2(srcname, dstname) # catch the Error from the recursive copytree so that we can # continue with other files except shutil.Error, err: errors.extend(err.args[0]) except EnvironmentError, why: errors.append((srcname, dstname, str(why))) try: shutil.copystat(src, dst) except OSError, why: if WindowsError is not None and isinstance(why, WindowsError): # Copying file access times may fail on Windows pass else: errors.append((src, dst, str(why))) #if errors: # raise shutil.Error, errors def _copy_build_output(self, src, dst): if not os.path.isdir(src) : return try: self.copytree2(src, dst, ignore=shutil.ignore_patterns('*.edc', '*.po', 'objs', '*.info', '*.so', 'CMakeLists.txt', '*.h', '*.c')) except OSError as exc: # File already exist if exc.errno == errno.EEXIST: shutil.copy(src, dst) if exc.errno == errno.ENOENT: shutil.copy(src, dst) else: raise def _package_sharedlib(self, project_path, conf, app_name): """If -r option used for packaging, make zip file from copied files""" #project_path=project['path'] project_build_output_path=os.path.join(project_path, conf) package_path=os.path.join(project_build_output_path, '.pkg') if os.path.isdir(package_path): shutil.rmtree(package_path) os.makedirs(package_path) os.makedirs(os.path.join(package_path, 'lib')) #Copy project resource self._copy_build_output(os.path.join(project_path, 'lib'), os.path.join(package_path, 'lib')) self._copy_build_output(os.path.join(project_path, 'res'), os.path.join(package_path, 'res')) #Copy built res resource self._copy_build_output(os.path.join(project_build_output_path, 'res'), os.path.join(package_path, 'res')) #Copy so library file for filename in list_files(project_build_output_path, 'so'): shutil.copy(filename, os.path.join(package_path, 'lib')) # Copy so library file zipname=app_name + '.zip' rsrc_zip = os.path.join(project_build_output_path, zipname) myZipFile = zipfile.ZipFile(rsrc_zip, 'w') for filename in list_files(package_path): try: myZipFile.write(filename, filename.replace(package_path, '')) except Exception, e: print str(e) myZipFile.close() return rsrc_zip def build_native(self, source, rootstrap=None, arch=None, conf='Debug'): """SDK CLI build command""" _rootstrap = self.check_rootstrap(rootstrap) if _rootstrap == None: raise LocalError('Rootstrap %s not exist' % rootstrap) if rootstrap is None and arch is None: rootstrap = _rootstrap self.arch = 'x86' elif arch is None: if 'emulator64' in rootstrap: self.arch = 'x86_64' elif 'device64' in rootstrap: self.arch = 'aarch64' elif 'emulator' in rootstrap: self.arch = 'x86' elif 'device' in rootstrap: self.arch = 'arm' elif rootstrap is None: if arch not in ['x86', 'arm', 'aarch64', 'x86_64']: raise LocalError('Architecture and rootstrap mismatch') rootstrap = _rootstrap if arch == 'x86_64': rootstrap = rootstrap.replace('emulator', 'emulator64') elif arch == 'aarch64': rootstrap = rootstrap.replace('emulator', 'device64') elif arch == 'arm': rootstrap = rootstrap.replace('emulator', 'device') for x in source.project_list: out = self._run('build-native', ['-r', rootstrap, '-a', self.arch, '-C', conf, '-c', 'gcc', '--' , x['path']], checker=True) logpath = os.path.join(source.output_dir, \ 'build_%s_%s' % (rootstrap, os.path.basename(x['path']))) if not os.path.isdir(source.output_dir): os.makedirs(source.output_dir) with open(logpath, 'w') as lf: lf.write(out) ret = self.error_parser.check(out) if ret: with open(logpath+'.log', 'w') as lf: lf.write(out) raise LocalError(ret) def package(self, source, cert=None, pkg_type=None, conf=None): """SDK CLI package command""" if cert is None: cert = 'ABS' if pkg_type is None: pkg_type = 'tpk' if conf is None: conf = 'Debug' final_app = '' main_args = ['-t', pkg_type, '-s', cert] out = '' #logfile if conf == 'Release' : main_args.extend(['--strip', 'on']) for i, x in enumerate(source.project_list): if x['type'] == 'app': out = '%s\n%s' % (out, \ self._run('package', main_args + ['--',os.path.join(x['path'],conf)])) try: final_app = list_files(os.path.join(x['path'], conf), ext='tpk')[0] except: raise LocalError('TPK file not generated for %s.' % x['APPNAME']) x['out_package'] = final_app elif x['type'] == 'sharedLib': self._package_sharedlib(x['path'], conf, x['APPNAME']) x['out_package'] = list_files(os.path.join(x['path'], conf), ext='zip')[0] else: raise LocalError('Not supported project type %s' % x['type']) if source.b_multi == True: extra_args=[] print 'THIS IS MULTI PROJECT' for i, x in enumerate(source.project_list): if x['out_package'] != final_app and x['type'] == 'app': extra_args.extend(['-r', x['out_package']]) elif x['type'] == 'sharedLib': extra_args.extend(['-r', x['out_package']]) extra_args.extend(['--', final_app]) out = self._run('package', main_args + extra_args) #TODO: signature validation check failed : Invalid file reference. An unsigned file was found. print 'Packaging final step again!' out = self._run('package', main_args + ['--', final_app]) #Copy tpk to output directory shutil.copy(final_app, source.output_dir) def package_new(self, source, cert=None, pkg_type=None, conf=None, manual_strip=False): """SDK CLI package command IF Debug + Manual Strip off then generate package-name-debug.tpk IF Debug + Manual Strip on then generate package-name.tpk with custom strip IF Release then generate package-name.tpk with strip option """ if cert is None: cert = 'ABS' if pkg_type is None: pkg_type = 'tpk' if conf is None: conf = 'Debug' final_app = '' main_args = ['-t', pkg_type, '-s', cert] out = '' #logfile # remove tpk or zip file on project path package_list = [] for i, x in enumerate(source.project_list): package_list.extend(list_files(os.path.join(x['path'], conf), ext='tpk')) package_list.extend(list_files(os.path.join(x['path'], conf), ext='zip')) for k in package_list : print ' package list ' + k; os.remove(k) # Manual strip if manual_strip == True : strip_cmd=''; if self.arch == None: raise LocalError('Architecture is Noen') elif self.arch == 'x86' : strip_cmd = os.path.join(os.path.dirname(self.tizen), '../../i386-linux-gnueabi-gcc-4.9/bin/i386-linux-gnueabi-strip') elif self.arch == 'arm' : strip_cmd = os.path.join(os.path.dirname(self.tizen), '../../arm-linux-gnueabi-gcc-4.9/bin/arm-linux-gnueabi-strip') elif self.arch == 'x86_64' : strip_cmd = os.path.join(os.path.dirname(self.tizen), '../../x86_64-linux-gnu-gcc-4.9/bin/x86_64-linux-gnu-strip') elif self.arch == 'aarch64' : strip_cmd = os.path.join(os.path.dirname(self.tizen), '../../aarch64-linux-gnu-gcc-4.9/bin/aarch64-linux-gnu-strip') print strip_cmd for i, x in enumerate(source.project_list): dir = os.path.join(x['path'], conf) files = [os.path.join(dir,f) for f in os.listdir(dir) if os.path.isfile(os.path.join(dir,f))] for k in files: cmdline = strip_cmd + ' ' + k; #print 'my command line ' + cmdline; Executor().run(cmdline, show=False) elif conf == 'Release': main_args.extend(['--strip', 'on']) for i, x in enumerate(source.project_list): if x['type'] == 'app': out = '%s\n%s' % (out, \ self._run('package', main_args + ['--',os.path.join(x['path'],conf)])) try: final_app = list_files(os.path.join(x['path'], conf), ext='tpk')[0] except: raise LocalError('TPK file not generated for %s.' % x['APPNAME']) x['out_package'] = final_app elif x['type'] == 'sharedLib': self._package_sharedlib(x['path'], conf, x['APPNAME']) x['out_package'] = list_files(os.path.join(x['path'], conf), ext='zip')[0] else: raise LocalError('Not supported project type %s' % x['type']) if source.b_multi == True: extra_args=[] print 'THIS IS MULTI PROJECT' for i, x in enumerate(source.project_list): if x['out_package'] != final_app and x['type'] == 'app': extra_args.extend(['-r', x['out_package']]) elif x['type'] == 'sharedLib': extra_args.extend(['-r', x['out_package']]) extra_args.extend(['--', final_app]) out = self._run('package', main_args + extra_args) #TODO: signature validation check failed : Invalid file reference. An unsigned file was found. print 'Packaging final step again!' out = self._run('package', main_args + ['--', final_app]) #Copy tpk to output directory if conf == 'Debug' and manual_strip == False : basename = os.path.splitext(final_app)[0] newname = basename +'-debug.tpk' os.rename(final_app, newname) shutil.copy(newname, source.output_dir) else : shutil.copy(final_app, source.output_dir) def clean(self, source): """SDK CLI clean command""" if os.path.isdir(source.multizip_path): shutil.rmtree(source.multizip_path) if os.path.isfile(os.path.join(source.multizip_path, '.zip')): os.remove(os.path.join(source.multizip_path, '.zip')) for x in source.project_list: self._run('clean', ['--', x['path']], show=False) class Source(object): """Project source related job""" workspace = '' #Project root directory project_list = [] b_multi = False multi_conf_file = 'WORKSPACE' #Assume multi-project if this file exist. multizip_path = '' #For multi-project packaging -r option property_dict = {} output_dir = '_abs_out_' def __init__(self, src=None): if src == None: self.workspace = os.getcwd() else: self.workspace = os.path.abspath(src) self.output_dir = os.path.join(self.workspace, self.output_dir) os.environ['workspace_loc']=str(os.path.realpath(self.workspace)) self.multizip_path = os.path.join(self.workspace, 'multizip') self.pre_process() def set_properties(self, path): """Fetch all properties from project_def.prop""" mydict = {} cp = ConfigParser.SafeConfigParser() cp.optionxform = str cp.readfp(FakeSecHead(open(os.path.join(path, 'project_def.prop')))) for x in cp.items('ascection'): mydict[x[0]] = x[1] mydict['path'] = path return mydict def pre_process(self): if os.path.isfile(os.path.join(self.workspace, self.multi_conf_file)): self.b_multi = True with open(os.path.join(self.workspace, self.multi_conf_file)) as f: for line in f: file_path = os.path.join(self.workspace, line.rstrip()) self.project_list.append(self.set_properties(file_path)) else: self.b_multi = False file_path = os.path.join(self.workspace) self.project_list.append(self.set_properties(file_path)) def argument_parsing(argv): """Any arguments passed from user""" parser = argparse.ArgumentParser(description='ABS command line interface') subparsers = parser.add_subparsers(dest='subcommands') #### [subcommand - BUILD] #### build = subparsers.add_parser('build') build.add_argument('-w', '--workspace', action='store', dest='workspace', \ help='source directory') build.add_argument('-r', '--rootstrap', action='store', dest='rootstrap', \ help='(ex, mobile-3.0-device.core) rootstrap name') build.add_argument('-a', '--arch', action='store', dest='arch', \ help='(x86|arm|x86_64|aarch64) Architecture to build') build.add_argument('-t', '--type', action='store', dest='type', \ help='(tpk|wgt) Packaging type') build.add_argument('-s', '--cert', action='store', dest='cert', \ help='(ex, ABS) Certificate profile name') build.add_argument('-c', '--conf', action='store',default='Release', dest='conf', \ help='(ex, Debug|Release) Build Configuration') build.add_argument('--sdkpath', action='store', dest='sdkpath', \ help='Specify Tizen SDK installation root (one time init).' \ ' ex) /home/yours/tizen-sdk/') return parser.parse_args(argv[1:]) def build_main(args): """Command [build] entry point.""" my_source = Source(src=args.workspace) my_sdk = Sdk(sdkpath=args.sdkpath) my_sdk.clean(my_source) my_sdk.build_native(my_source, rootstrap=args.rootstrap, arch=args.arch, conf=args.conf) if args.conf == 'Debug' : my_sdk.package_new(my_source, pkg_type=args.type, cert=args.cert, conf=args.conf) my_sdk.package_new(my_source, pkg_type=args.type, cert=args.cert, conf=args.conf, manual_strip=True) else : my_sdk.package_new(my_source, pkg_type=args.type, cert=args.cert, conf=args.conf) def main(argv): """Script entry point.""" print 'ABS SCRIPT FROM GIT' args = argument_parsing(argv) if args.subcommands == 'build': return build_main(args) else: print 'Unsupported command %s' % args.subcommands raise LocalError('Command %s not supported' % args.subcommands) if __name__ == '__main__': try: sys.exit(main(sys.argv)) except Exception, e: print 'Exception %s' % str(e) sys.exit(1)