Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 596 lines (462 sloc) 20.7 KB
#!/usr/bin/env python2
import argparse
import subprocess
import shlex
import os
import sys
import json
import shutil
import platform
import xml.etree.ElementTree as ElementTree
# TODO generate clickable.json file
# TODO add Golang template
# TODO add desktop arch
# TODO make a snap
# TODO make sure lxd & lxd container is started
# TODO don't print out a stack trace for CTRL+C and subprocess errors
# TODO lxd setup shouldn't require a clickable.json
# TODO check for a template based on CMake files, etc
# TODO add command to uninstall
class Config(object):
package = None
app = None
sdk = 'ubuntu-sdk-15.04'
arch = 'armhf'
template = 'pure'
premake = None
postmake = None
prebuild = None
build = None
postbuild = None
launch = None
dir = './build/'
ssh = False
kill = None
scripts = {}
chroot = False
default = 'kill clean build click-build install launch'
log = None
specificDependencies = False
dependencies = []
ignore = []
required = ['sdk', 'arch', 'template', 'dir']
keys = [
'package', 'app', 'sdk', 'arch', 'prebuild', 'template', 'premake',
'postmake', 'build', 'postbuild', 'launch', 'dir', 'ssh', 'kill', 'scripts',
'chroot', 'default', 'log', 'specificDependencies', 'dependencies', 'ignore'
]
PURE_QML_QMAKE = 'pure-qml-qmake'
QMAKE = 'qmake'
PURE_QML_CMAKE = 'pure-qml-cmake'
CMAKE = 'cmake'
CUSTOM = 'custom'
CORDOVA = 'cordova'
PURE = 'pure'
templates = [PURE_QML_CMAKE, QMAKE, PURE_QML_CMAKE, CMAKE, CUSTOM, CORDOVA, PURE]
def __init__(self, ip=None, arch=None, template=None):
self.cwd = os.getcwd()
self.load_config()
if ip:
self.ssh = ip
if arch:
self.arch = arch
if template:
self.template = template
if not self.kill:
if self.template == self.CORDOVA:
self.kill = 'cordova-ubuntu'
elif self.template == self.PURE_QML_CMAKE or self.template == self.PURE_QML_QMAKE or self.template == self.PURE:
self.kill = 'qmlscene'
else:
self.kill = self.app
if self.template == self.PURE_QML_CMAKE or self.template == self.PURE_QML_QMAKE or self.template == self.PURE:
self.arch = 'all'
if self.template == self.CUSTOM and not self.build:
raise ValueError('When using the "custom" template you must specify a "build" in the config')
if self.template not in self.templates:
raise ValueError('"{}" is not a valid template ({})'.format(self.template, ', '.join(self.templates)))
def load_config(self, file='clickable.json'):
if os.path.isfile(os.path.join(self.cwd, file)):
with open(os.path.join(self.cwd, file), 'r') as f:
config_json = {}
try:
config_json = json.load(f)
except ValueError:
raise ValueError('Failed reading "{}", it is not valid json'.format(file))
for key in self.keys:
value = config_json.get(key, None)
if value:
setattr(self, key, value)
else:
print('No clickable.json was found, using defaults and cli args')
for key in self.required:
if not getattr(self, key):
raise ValueError('"{}" is empty in the config file'.format(key))
self.dir = os.path.abspath(self.dir)
class Clickable(object):
cwd = None
def __init__(self, config, device_serial_number=None):
self.cwd = os.getcwd()
self.config = config
self.temp = self.config.dir + '/tmp'
self.device_serial_number = device_serial_number
if type(self.device_serial_number) == type([]) and len(self.device_serial_number) > 0:
self.device_serial_number = self.device_serial_number[0]
self.host_arch = 'amd64' if platform.architecture()[0] == '64bit' else 'i386'
self.build_arch = self.config.arch
if self.config.template == self.config.PURE_QML_QMAKE or self.config.template == self.config.PURE_QML_CMAKE or self.config.PURE:
self.build_arch = 'armhf'
def find_manifest(self):
# TODO the manifest might not be in the build directory, check in the src directory
# TODO write this in python
manifest = ''
if self.config.template == self.config.PURE:
manifest = subprocess.check_output('find . -name "manifest.json" -print', cwd=self.config.dir, shell=True)
else:
manifest = subprocess.check_output('find . -path ./tmp -prune -o -name "manifest.json" -print', cwd=self.config.dir, shell=True)
# TODO error if not found
return os.path.join(self.config.dir, manifest.strip())
def get_manifest(self):
manifest = {}
with open(self.find_manifest(), 'r') as f:
try:
manifest = json.load(f)
except ValueError:
raise ValueError('Failed reading "manifest.json", it is not valid json')
return manifest
def find_version(self):
return self.get_manifest().get('version', '1.0')
def find_package_name(self):
package = self.config.package
if not package:
package = self.get_manifest().get('name', None)
if not package:
raise ValueError('No package name specified in manifest.json or clickable.json')
return package
def find_app_name(self):
app = self.config.app
if not app:
hooks = self.get_manifest().get('hooks', {})
for key, value in hooks.items():
if 'desktop' in value:
app = key
break
if not app: # If we don't find an app with a desktop file just find the first one
apps = list(hooks.keys())
if len(apps) > 0:
app = apps[0]
if not app:
raise ValueError('No app name specified in manifest.json or clickable.json')
return app
def run_device_command(self, command):
wrapped_command = ''
if self.config.ssh:
wrapped_command = 'echo "{}" | ssh phablet@{}'.format(command, self.config.ssh)
else:
if self.device_serial_number:
wrapped_command = 'adb -s {} shell "{}"'.format(self.device_serial_number, command)
else:
wrapped_command = 'adb shell "{}"'.format(command)
subprocess.check_call(wrapped_command, cwd=self.config.dir, shell=True)
def run_container_command(self, command, force_lxd=False, sudo=False, get_output=False, use_dir=True):
wrapped_command = command
if self.config.chroot and not force_lxd:
chroot_command = 'run'
if sudo:
chroot_command = 'maint'
wrapped_command = 'click chroot -a {} -f {} {} {}'.format(self.build_arch, self.config.sdk, chroot_command, command)
else:
if not self.check_lxd():
raise Exception('No lxd container exists to build in, please run `clickable setup-lxd`')
target_command = 'exec'
if sudo:
target_command = 'maint'
if use_dir:
command = 'cd {}; {}'.format(self.config.dir, command)
wrapped_command = 'usdk-target {} clickable-{} -- bash -c "{}"'.format(target_command, self.build_arch, command)
kwargs = {}
if use_dir:
kwargs['cwd'] = self.config.dir
if get_output:
return subprocess.check_output(shlex.split(wrapped_command), **kwargs)
else:
subprocess.check_call(shlex.split(wrapped_command), **kwargs)
def setup_dependencies(self):
if len(self.config.dependencies) > 0:
print('Checking dependencies')
command = 'apt-get install -y --force-yes'
run = False
for dep in self.config.dependencies:
if self.config.arch == 'armhf' and 'armhf' not in dep and not config.specificDependencies:
dep = '{}:{}'.format(dep, self.config.arch)
exists = ''
try:
exists = self.run_container_command('dpkg -s {} | grep Status'.format(dep), get_output=True, use_dir=False)
except subprocess.CalledProcessError:
exists = ''
if exists.strip() != 'Status: install ok installed':
run = True
command = '{} {}'.format(command, dep)
if run:
self.run_container_command(command, sudo=True, use_dir=False)
else:
print('Dependencies already installed')
def check_lxd(self):
name = 'clickable-{}'.format(self.build_arch)
# Check for existing container
existing = subprocess.check_output(shlex.split('usdk-target list'))
existing = json.loads(existing)
found = False
for container in existing:
if container['name'] == name:
found = True
return found
def setup_lxd(self):
name = 'clickable-{}'.format(self.build_arch)
alias = '{}-{}-{}-dev'.format(self.config.sdk, self.host_arch, self.build_arch)
if not self.check_lxd():
print('Going to setup the lxd container')
# Find the image we want
images = subprocess.check_output(shlex.split('usdk-target images'))
images = json.loads(images)
fingerprint = None
for image in images:
if image['alias'] == alias:
fingerprint = image['fingerprint']
if not fingerprint:
raise Exception('The {} lxd image could not be found'.format(alias))
print('Asking for root to create the lxd container')
# Create a new container
subprocess.check_call(shlex.split('sudo usdk-target create -n {} -p {}'.format(name, fingerprint)))
self.run_container_command('apt-get update', force_lxd=True, sudo=True, use_dir=False)
self.run_container_command('apt-get install -y --force-yes click', force_lxd=True, sudo=True, use_dir=False)
def click_build(self):
command = 'click build {} --no-validate'.format(self.temp)
if self.config.chroot:
subprocess.check_call(shlex.split(command), cwd=self.config.dir)
else:
# Run this in the container so the host doesn't need to have click installed
self.run_container_command(command)
self.click_review()
def click_review(self):
pass # TODO implement this
def install(self):
click = '{}_{}_{}.click'.format(self.find_package_name(), self.find_version(), self.config.arch)
click_path = os.path.join(self.config.dir, click)
if self.config.ssh:
command = 'scp {} phablet@{}:/home/phablet/'.format(click_path, self.config.ssh)
subprocess.check_call(command, cwd=self.config.dir, shell=True)
else:
if self.device_serial_number:
command = 'adb -s {} push {} /home/phablet/'.format(self.device_serial_number, click_path)
else:
command = 'adb push {} /home/phablet/'.format(click_path)
subprocess.check_call(command, cwd=self.config.dir, shell=True)
self.run_device_command('pkcon install-local --allow-untrusted {}'.format(click))
def kill(self):
if self.config.kill:
try:
self.run_device_command('pkill -f {}'.format(self.config.kill))
except:
pass # Nothing to do, the process probably wasn't running
def launch(self):
launch = 'ubuntu-app-launch {}_{}_{}'.format(self.find_package_name(), self.find_app_name(), self.find_version())
if self.config.launch:
launch = self.config.launch
self.run_device_command('sleep 1s && {}'.format(launch))
def logs(self):
# TODO Support scope logs
log = '~/.cache/upstart/application-click-{}_{}_{}.log'.format(self.find_package_name(), self.find_app_name(), self.find_version())
if self.config.log:
log = self.config.log
self.run_device_command('tail -f {}'.format(log))
def clean(self):
try:
shutil.rmtree(self.config.dir)
except:
type, value, traceback = sys.exc_info()
if type == OSError and 'No such file or directory' in value: # TODO see if there is a proper way to do this
pass # Nothing to do here, the directory didn't exist
else:
print('Failed to clean the build directory: {}: {}'.format(type, value))
try:
shutil.rmtree(self.temp)
except:
type, value, traceback = sys.exc_info()
if type == OSError and 'No such file or directory' in value: # TODO see if there is a proper way to do this
pass # Nothing to do here, the directory didn't exist
else:
print('Failed to clean the temp directory: {}: {}'.format(type, value))
def _build(self):
raise NotImplementedError()
def build(self):
try:
os.makedirs(self.config.dir)
except:
print('Failed to create the build directory: {}'.format(str(sys.exc_info()[0])))
self.setup_dependencies()
if self.config.prebuild:
subprocess.check_call(self.config.prebuild, cwd=self.cwd, shell=True)
self._build()
if self.config.postbuild:
subprocess.check_call(self.config.postbuild, cwd=self.config.dir, shell=True)
def script(self, name, device=False):
if name in self.config.scripts:
if device:
self.run_device_command('{}'.format(self.config.scripts[name]))
else:
subprocess.check_call(self.config.scripts[name], cwd=self.cwd, shell=True)
class MakeClickable(Clickable):
def pre_make(self):
if self.config.premake:
subprocess.check_call(self.config.premake, cwd=self.config.dir, shell=True)
def post_make(self):
if self.config.postmake:
subprocess.check_call(self.config.premake, cwd=self.config.dir, shell=True)
def make(self):
self.run_container_command('make')
def make_install(self):
if os.path.exists(self.temp) and os.path.isdir(self.temp):
shutil.rmtree(self.temp)
try:
os.makedirs(self.temp)
except:
print('Failed to create temp dir ({}): {}'.format(self.temp, str(sys.exc_info()[0])))
# The actual make command is implemented in the subclasses
def _build(self):
self.pre_make()
self.make()
self.post_make()
self.make_install()
class CMakeClickable(MakeClickable):
def make_install(self):
super(CMakeClickable, self).make_install()
self.run_container_command('make DESTDIR={} install'.format(self.temp))
def _build(self):
self.run_container_command('cmake {}'.format(self.cwd))
super(CMakeClickable, self)._build()
class QMakeClickable(MakeClickable):
def make_install(self):
super(QMakeClickable, self).make_install()
self.run_container_command('make INSTALL_ROOT={} install'.format(self.temp))
def _build(self):
if self.build_arch == 'armhf':
self.run_container_command('qt5-qmake-arm-linux-gnueabihf {}'.format(self.cwd))
else:
# TODO implement this
raise Exception('{} is not supported by the qmake build yet'.format(self.build_arch))
super(QMakeClickable, self)._build()
class CustomClickable(Clickable):
def _build(self):
self.run_container_command(self.config.build)
class PureQMLMakeClickable(MakeClickable):
def post_make(self):
super(PureQMLMakeClickable, self).post_make()
with open(self.find_manifest(), 'r') as f:
manifest = {}
try:
manifest = json.load(f)
except ValueError:
raise ValueError('Failed reading "manifest.json", it is not valid json')
manifest['architecture'] = 'all'
with open(self.find_manifest(), 'w') as writer:
json.dump(manifest, writer, indent=4)
class PureQMLQMakeClickable(PureQMLMakeClickable, QMakeClickable):
pass
class PureQMLCMakeClickable(PureQMLMakeClickable, CMakeClickable):
pass
class PureClickable(Clickable):
def _ignore(self, path, contents):
ignored = []
for content in contents:
cpath = os.path.abspath(os.path.join(path, content))
if cpath == os.path.abspath(self.temp) or cpath == os.path.abspath(self.config.dir) or content in self.config.ignore or content == 'clickable.json':
ignored.append(content)
return ignored
def _build(self):
shutil.copytree(self.cwd, self.temp, ignore=self._ignore)
print('Copied files to temp directory for click building')
class CordovaClickable(Clickable):
def _build(self):
command = "cordova -d build ubuntu --device -- --framework={}".format(self.config.sdk)
subprocess.check_call(shlex.split(command), cwd=self.cwd)
def click_build(self):
click = '{}_{}_{}.click'.format(self.find_package_name(), self.find_version(), self.config.arch)
src = '{}/platforms/ubuntu/{}/{}/prefix/{}'.format(self.cwd, self.config.sdk, self.config.arch, click)
dest = '{}/{}'.format(self.config.dir, click)
shutil.copyfile(src, dest)
def find_package_name(self):
tree = ElementTree.parse('config.xml')
root = tree.getroot()
return root.attrib['id'] if 'id' in root.attrib else '1.0.0'
def find_version(self):
tree = ElementTree.parse('config.xml')
root = tree.getroot()
return root.attrib['version'] if 'version' in root.attrib else '1.0.0'
if __name__ == "__main__":
config = None
COMMAND_HANDLERS = {
"kill": "kill",
"clean": "clean",
"build": "build",
"click_build": "click_build",
"click-build": "click_build",
"build_click": "click_build",
"build-click": "click_build",
"install": "install",
"launch": "launch",
"logs": "logs",
"setup-lxd": "setup_lxd"
}
def show_valid_commands():
n = [
'Valid commands:',
", ".join(sorted(COMMAND_HANDLERS.keys()))
]
if config and hasattr(config, "scripts") and config.scripts:
n += [
'Project-specific custom commands:',
", ".join(sorted(config.scripts.keys()))
]
return "\n".join(n)
def print_valid_commands(): print(show_valid_commands())
# TODO better help text & version
parser = argparse.ArgumentParser(description='clickable')
parser.add_argument('commands', nargs='*', help=show_valid_commands())
parser.add_argument('--device', '-d', action="store_true", default=False)
parser.add_argument('--device-serial-number', '-s',
help="directs command to the device or emulator with the given serial number or qualifier",
nargs=1, default=None)
parser.add_argument('--ip', '-i')
parser.add_argument('--arch', '-a')
parser.add_argument('--template', '-t')
args = parser.parse_args()
config = Config(ip=args.ip, arch=args.arch, template=args.template)
clickable = None
if config.template == config.PURE_QML_QMAKE:
clickable = PureQMLQMakeClickable(config, args.device_serial_number)
elif config.template == config.QMAKE:
clickable = QMakeClickable(config, args.device_serial_number)
elif config.template == config.PURE_QML_CMAKE:
clickable = PureQMLCMakeClickable(config, args.device_serial_number)
elif config.template == config.CMAKE:
clickable = CMakeClickable(config, args.device_serial_number)
elif config.template == config.CUSTOM:
clickable = CustomClickable(config, args.device_serial_number)
elif config.template == config.CORDOVA:
clickable = CordovaClickable(config, args.device_serial_number)
elif config.template == config.PURE:
clickable = PureClickable(config, args.device_serial_number)
commands = args.commands
if len(args.commands) == 0:
commands = config.default.split(' ')
for command in commands:
if command in config.scripts:
clickable.script(command, args.device)
elif command in COMMAND_HANDLERS:
getattr(clickable, COMMAND_HANDLERS[command])()
elif command == "help":
parser.print_help()
else:
print('There is no builtin or custom command named "{}"'.format(command))
print_valid_commands()