summaryrefslogtreecommitdiffstats
path: root/tests/src/lib
diff options
context:
space:
mode:
authorDenis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>2020-09-28 17:14:55 +0200
committerDenis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>2021-07-14 17:00:22 +0200
commitb3b9dae5d21750bc281f4482235d84de19367b4a (patch)
tree7ce3bbd0db91139705722f81a523deb39957c565 /tests/src/lib
parent7e4e660803408d5ab01a1a72262797a054fa5fa4 (diff)
downloadvendor_replicant-scripts-b3b9dae5d21750bc281f4482235d84de19367b4a.tar.gz
vendor_replicant-scripts-b3b9dae5d21750bc281f4482235d84de19367b4a.tar.bz2
vendor_replicant-scripts-b3b9dae5d21750bc281f4482235d84de19367b4a.zip
Add crude python library and example programs to do automatic installations
Here's how to test the Replicant 6.0 0004 RC5 data migration: $ cd tests/src $ ./test-replicant-6.0-0004-rc5-migration.py n7100 [ OK ] Installed Replicant 6.0 0003 [ OK ] Installed APKs on top of Replicant 6.0 0003 [ OK ] Installed Replicant 6.0 0004-rc5-transition [ OK ] Installed Replicant 6.0 0004-rc5 There is still a lot to be done as the libraries still have many things that are not implemented yet. For instance it doesn't support yet all the Smartphones and tablets supported by Replicant 6.0 0003. There is also a lot of TODO in the source code. In addition, the loggin system isn't very flexible. Signed-off-by: Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
Diffstat (limited to 'tests/src/lib')
-rw-r--r--tests/src/lib/__init__.py0
-rw-r--r--tests/src/lib/bootloaders.py201
-rw-r--r--tests/src/lib/common.py39
-rw-r--r--tests/src/lib/config.py92
-rw-r--r--tests/src/lib/devices.py207
-rw-r--r--tests/src/lib/fdroid.py60
-rw-r--r--tests/src/lib/heimdall.py68
-rw-r--r--tests/src/lib/hosts.py67
-rw-r--r--tests/src/lib/releases.py206
-rw-r--r--tests/src/lib/replicant_releases.py260
-rw-r--r--tests/src/lib/replicant_source.py127
-rw-r--r--tests/src/lib/test.py117
12 files changed, 1444 insertions, 0 deletions
diff --git a/tests/src/lib/__init__.py b/tests/src/lib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/src/lib/__init__.py
diff --git a/tests/src/lib/bootloaders.py b/tests/src/lib/bootloaders.py
new file mode 100644
index 0000000..35163db
--- /dev/null
+++ b/tests/src/lib/bootloaders.py
@@ -0,0 +1,201 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import os
+import re
+import sh
+import time
+
+from lib.common import get_output
+from lib.common import trace
+from lib.heimdall import Heimdall
+
+# TODO: Derive that information from the PIT in the data repository instead
+class BootloaderRuntime(object):
+ def __init__(self, config, host, device, quiet=False):
+ self.config = config
+ self.device = device
+ self.host = host
+ self.quiet = quiet
+ self.heimdall = Heimdall(self.host, quiet=quiet)
+
+ @trace
+ def get_cache_partition(self):
+ if not self.quiet:
+ print("BootloaderRuntime.get_cache_partition()")
+
+ if str(self.device) in ['espresso3g', 'espressowifi', 'i9100', 'i9300',
+ 'i9305', 'n5100', 'n5110', 'n7000', 'n7100']:
+ return 'CACHE'
+ elif str(self.device) in ['maguro']:
+ return 'cache'
+ else:
+ assert(False)
+
+ @trace
+ def get_recovery_partition(self):
+ if str(self.device) in ['espresso3g', 'espressowifi', 'i9100', 'i9300',
+ 'i9305', 'n5100', 'n5110', 'n7000', 'n7100']:
+ return 'RECOVERY'
+ elif str(self.device) in ['maguro']:
+ return 'recovery'
+ else:
+ assert(False)
+
+ @trace
+ def get_boot_img_partition(self):
+ if str(self.device) in ['espresso3g', 'espressowifi', 'i9100',
+ 'n7000']:
+ return 'KERNEL'
+ elif str(self.device) in ['i9300', 'i9305', 'n5100', 'n5110', 'n7100']:
+ return 'BOOT'
+ elif str(self.device) in ['maguro']:
+ return 'boot'
+ else:
+ assert(False)
+
+ @trace
+ def is_ready(self):
+ return self.heimdall.is_ready()
+
+ @trace
+ def wait_for_ready(self):
+ while True:
+ if self.is_ready():
+ return True
+ else:
+ time.sleep(1)
+
+ @trace
+ def boot_to_bootloader(self):
+ device_offline = False
+ while True:
+ if self.is_ready():
+ return True
+
+ adb_state = None
+ try:
+ adb_state = get_output(self.host.run(['adb', 'get-state']))
+ except sh.ErrorReturnCode_1:
+ if not device_offline and not self.quiet:
+ print('\tNo devices detected. Possible causes:')
+ print('\t- ADB is not enabled? '
+ '(device booted in Replicant with adb off)')
+ print('\t- No devices are connected (bad cable?)')
+ print('\t- The device is off '
+ '(no powered on device detected)')
+ print('\t- The device is booting '
+ 'and adbd is not yet started')
+ device_offline = True
+ time.sleep(1)
+
+ if adb_state in ['device', 'recovery']:
+ self.host.run(['adb', 'reboot', 'bootloader'])
+ return self.wait_for_ready()
+
+ @trace
+ def install_recovery(self, path, retries=100):
+ if not self.quiet:
+ print('Starting to install the Recovery on {}'.format(self.device))
+
+ self.boot_to_bootloader()
+
+ partitions = {
+ self.get_boot_img_partition() : path,
+ self.get_recovery_partition() : path,
+ }
+
+ return self.heimdall.flash(partitions, retries)
+
+ @trace
+ def install_boot_img(self, path, retries=100):
+ if not self.quiet:
+ print('Starting to install the {} boot image on {}'.format(
+ path, self.device))
+
+ self.boot_to_bootloader()
+
+ partitions = {
+ self.get_boot_img_partition() : path,
+ }
+
+ return self.heimdall.flash(partitions)
+
+ @trace
+ def wipe_cache(self):
+ # It currently fails with the following error with a file of 8M on the
+ # Galaxy SIII (GT-I9300):
+ # Uploading CACHE
+ # 100%Ending session...
+ # R... (30 more, please see e.stdout)
+ #
+ # STDERR:
+ #
+ # ERROR: Failed to confirm end of file transfer sequence!
+ # ERROR: CACHE upload failed!
+ #
+ # ERROR: Failed to send end session packet!
+ assert(False)
+ # The code responsible for that failure has been kept below for
+ # reference and to enable to fix it.
+
+ # The protocol that heimdall uses is called Thor
+ # On all Replicant compatible devices with an Exynos, the nonfree
+ # bootloader (s-boot 4.0) has a very buggy implementation of Thor.
+ # Practically speaking:
+ # - If your computer is under heavy I/O and/or CPU load, it increases
+ # the probability of the transfer failling.
+ # - The probability of the transfer failing also increases with the
+ # increase of the image size.
+ # So we create a small file. On devices with other SOCs, like the Galaxy
+ # Nexus (GT-I9250) or the Galaxy Tab 2 (GT-P3100, GT-P3110, GT-P5100,
+ # GT-P5510), the stock bootloader seem to have implemented that protocol
+ # correctly. Upstream U-boot also has a free software implementation of
+ # that protocol.
+ cache_file_size = 8 * 1024 *1024
+ cache_partition_name = self.get_cache_partition()
+ tmpdir = self.host.mktemp()
+
+ part_img_path = '{}{}{}.img'.format(tmpdir, os.sep, cache_partition_name)
+
+ self.host.run(['dd',
+ 'if=/dev/zero',
+ 'of={}'.format(part_img_path),
+ 'count={}'.format(int(cache_file_size / 512))])
+
+ self.boot_to_bootloader()
+
+ partitions = {
+ cache_partition_name : part_img_path,
+ }
+
+ return self.heimdall.flash(partitions)
+
+ @trace
+ def run(self, commands):
+ # Remove that assert if you really want to run commands inside the
+ # bootloader.
+ # For now we support various nonfree bootloader that can run commands
+ # through the UART port and code through an exploit through USB.
+ # While it would probably not be easy to run bootloaders commands
+ # through USB, it might be possible to manipulate the PARAM partition
+ # to implement a subset of the functionalities.
+ assert(False)
+
+ @trace
+ def boot(self, environment, retries=100):
+ if environment == 'bootloader':
+ return self.heimdall.reboot()
diff --git a/tests/src/lib/common.py b/tests/src/lib/common.py
new file mode 100644
index 0000000..1c6d6c6
--- /dev/null
+++ b/tests/src/lib/common.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import os
+import re
+
+trace_enabled = False
+
+def get_output(running_command):
+ return re.sub(os.linesep + '$', '', str(running_command))
+
+def enable_trace(enabled):
+ global trace_enabled
+ trace_enabled = enabled
+
+def trace(func):
+ def trace_func(*args, **kwargs):
+ print("+ {}()".format(func.__qualname__))
+ result = func(*args, **kwargs)
+ print("- {}()".format(func.__qualname__))
+ return result
+ if trace_enabled:
+ return trace_func
+ else:
+ return func
+
diff --git a/tests/src/lib/config.py b/tests/src/lib/config.py
new file mode 100644
index 0000000..fcd5fdd
--- /dev/null
+++ b/tests/src/lib/config.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import configparser
+import os
+
+class ReplicantConfig(object):
+ def __init__(self):
+ # This should implement the XDG Base Directory Specification which is
+ # available here:
+ # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
+
+ config_file = None
+ xdg_config_home = ''
+ try:
+ xdg_config_home = os.environ['XDG_CONFIG_HOME']
+ except KeyError:
+ pass
+
+ if xdg_config_home == '':
+ try:
+ xdg_config_home = os.environ['HOME'] + os.sep + '.config'
+ except KeyError:
+ # This follow strictly the specification
+ xdg_config_home = os.sep + '.config'
+
+ xdg_config_dirs = ''
+ try:
+ xdg_config_dirs = os.environ['XDG_CONFIG_DIRS']
+ except KeyError:
+ pass
+ if xdg_config_dirs == '':
+ xdg_config_dirs = os.sep + 'etc' + os.sep + 'xdg'
+
+ for base in [xdg_config_home] + xdg_config_dirs.split(os.pathsep):
+ config_path = base + os.sep + 'replicant' + os.sep \
+ + 'replicant_tests.conf'
+ if not os.path.isfile(config_path):
+ continue
+ try:
+ # Silently skip the file in case of issues as per the
+ # specification
+ config_file = open(config_path, 'r')
+ except:
+ pass
+
+ if config_file:
+ break
+
+ if config_file is None:
+ # TODO: raise some error
+ print("Configuration file not found")
+ assert(False)
+
+ self.config = configparser.ConfigParser()
+ self.config.read_file(config_file)
+
+ # The config parameter corresponds to the sections in the configuration
+ # file. At the time of writing we have the following sections:
+ # - replicant-builder
+ # - replicant-installer
+ # - fdroid-installer
+ def get(self, section):
+ if section not in ['replicant-builder',
+ 'replicant-installer',
+ 'fdroid-installer']:
+ # TODO
+ assert(False)
+
+ results = {}
+
+ replicant_installer_config = self.config[section]
+ for k, v in replicant_installer_config.items():
+ if v and v.startswith('~'):
+ results[k] = os.environ['HOME'] + v[1:]
+ else:
+ results[k] = v
+
+ return results
diff --git a/tests/src/lib/devices.py b/tests/src/lib/devices.py
new file mode 100644
index 0000000..be271e7
--- /dev/null
+++ b/tests/src/lib/devices.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os
+import re
+import sh
+import time
+
+import lib.bootloaders
+import lib.replicant_releases
+
+from lib.common import get_output
+from lib.common import trace
+
+class Device(object):
+ def __init__(self, host, name, quiet=False):
+ self.host = host
+ self.name = name
+ self.quiet = quiet
+
+ def __str__(self):
+ # TODO: add codename <-> name
+ return self.name
+
+class DeviceRuntime(object):
+ def __init__(self, config, host, device, release, quiet=False):
+ self.device = device
+ self.config = config
+ self.host = host
+ self.quiet = quiet
+ self.release = release
+
+ self.bootloader = lib.bootloaders.BootloaderRuntime(config, host,
+ device, quiet=quiet)
+ self.recovery = lib.replicant_releases.RecoveryRuntime(config, host,
+ device, release,
+ quiet=quiet)
+ def run(self, commands):
+ # We have no visibility on the device state at this point
+ assert(False)
+
+ @trace
+ def state(self):
+ if self.bootloader.is_ready():
+ return 'bootloader'
+
+ adb_state = None
+ try:
+ # Available adb states: device, recovery, rescue, sideload,
+ # bootloader, and disconnect
+ adb_state = get_output(self.host.run(['adb', 'get-state']))
+ except sh.ErrorReturnCode_1:
+ return 'disconnect'
+ return adb_state
+
+ # If state is None: waits for a state other than disconnected
+ # else waits for the given state
+ @trace
+ def wait_for_state(self, given_state=None, retries=100):
+ while retries > 0:
+ new_state = self.state()
+ if new_state == 'disconnect':
+ continue
+ elif given_state == None:
+ return new_state
+ elif new_state != given_state:
+ continue
+ else:
+ return new_state
+ retries -= 1
+ time.sleep(1)
+
+ @trace
+ def wait_for_boot_complete(self, retries=100):
+ self.wait_for_state('device', retries)
+ while retries > 0:
+ output = get_output(self.host.run(['adb', 'shell',
+ 'getprop', 'dev.bootcomplete']))
+ if output == 1:
+ return True
+ retries -= 1
+ time.sleep(1)
+ return False
+
+ @trace
+ def wipe_cache_and_data(self):
+ # TODO: The bootloader is way less reliable for that, however the
+ # recovery could also be stuck in sideload mode without any adb
+ # access. In that case it would not be possible anymore to use
+ # adb to wipe the cache and data, so we'd need to use the bootloader.
+ return self.recovery.wipe_cache_and_data()
+
+ @trace
+ def install_zip(self, path, add_root=True, disable_modem=False):
+ self.recovery.install_zip(path)
+
+ # We can't simply reflash the boot.img from the bootloader because
+ # if we want to disable the modem without leaving root enabled, once
+ # we flashed back the stock boot.img, we would end up booting
+ # without any way to reboot in the recovery.
+ #
+ # And here we need to always end up in the recovery for consistency
+ # as simply installing a zip (without adding root or disabling the
+ # modem) still ends up in the recovery.
+ # So the solution here is to do as much as possible from within the
+ # recovery when adding root or disabling the modem
+ if add_root:
+ tmpdir = self.host.mktemp()
+ boot_img_path = tmpdir + os.sep + 'boot.img'
+ new_boot_img_path = boot_img_path + '.new'
+
+ self.host.run(['unzip', path, '-d', tmpdir])
+ self.host.run(['add_adb_root.py', boot_img_path, new_boot_img_path])
+
+ self.recovery.install_boot_img(new_boot_img_path)
+ self.host.run(['rm', '-f', new_boot_img_path])
+
+ if disable_modem:
+ # This uses the device fstab, and /system is standard so it should
+ # work on any device
+ self.host.run(['adb', 'shell', 'mount', '/system'])
+
+ # modem.sh off does basically the same thing, so modem.sh on will
+ # also work if the user wants to re-enable the modem
+ self.host.run(['adb', 'shell', 'mv', '/system/lib/libsamsung-ril.so', '/system/lib/libsamsung-ril.so.disabled'])
+
+ self.host.run(['adb', 'shell', 'umount', '/system'])
+ self.host.run(['adb', 'shell', 'sync'])
+
+ @trace
+ def install_apk(self, path):
+ self.wait_for_state('device')
+ self.host.run(['adb', 'install', path])
+
+ @trace
+ def get_adb_root_recovery(self, original_recovery_path):
+ if not self.quiet:
+ print('Starting to create a new recovery with adb root access:')
+
+ recovery_filename = original_recovery_path.split(os.sep)[-1]
+
+ tmpdir = get_output(self.host.run(['mktemp', '-d']))
+ new_recovery_path = tmpdir + os.sep + recovery_filename
+
+ self.host.run(['add_adb_root.py', original_recovery_path,
+ new_recovery_path])
+
+ if not os.path.isfile(original_recovery_path):
+ print("{}: {} not found".format("DeviceRuntime.get_adb_root_recovery",
+ original_recovery_path))
+ assert(False)
+ if not os.path.isfile(new_recovery_path):
+ print("{}: add_adb_root.py failed: {} not found".format(
+ "DeviceRuntime.get_adb_root_recovery",
+ original_recovery_path))
+ assert(False)
+
+ return new_recovery_path
+
+ @trace
+ def install_recovery(self, path, add_root, retries=10):
+ if add_root == True:
+ path = self.get_adb_root_recovery(path)
+
+ self.bootloader.install_recovery(path)
+
+ if add_root == True:
+ self.host.run(['rm', '-f', path])
+
+ # Possible values for environment:
+ # - bootloader
+ # - device
+ # - recovery
+ @trace
+ def boot(self, environment, retries=100):
+ while retries > 0:
+ state = self.state()
+ if not self.quiet:
+ print("\tDeviceRuntime.boot: state: {}".format(state))
+ if state == 'bootloader':
+ return self.bootloader.boot(environment)
+ elif state in ['device', 'recovery']:
+ return self.host.run(['adb', 'reboot', environment])
+ elif state == 'disconnect':
+ # The device can be in that state when adbd has not been started
+ # yet
+ pass
+ elif state in ['rescue', 'sideload']:
+ # TODO
+ assert(False)
+ else:
+ assert(False)
+ time.sleep(1)
+ retries -= 1
+ assert(False)
diff --git a/tests/src/lib/fdroid.py b/tests/src/lib/fdroid.py
new file mode 100644
index 0000000..c02f20c
--- /dev/null
+++ b/tests/src/lib/fdroid.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import os
+
+import lib.hosts
+
+from lib.common import trace
+
+class FDroidAPK(object):
+ def __init__(self, config, host, runtime, apk_name, quiet=False):
+ self.config = config
+ self.apk_name = apk_name
+ self.host = host
+ self.runtime = runtime
+
+ @trace
+ def download(self):
+ # TODO, also handles caching and host repository dir
+ pass
+
+ @trace
+ def get_path(self):
+ return self.config['releases_directory'] + os.sep + self.apk_name
+
+ @trace
+ def verify(self):
+ # TODO: verify signature
+ return True
+
+ @trace
+ def install(self):
+ self.verify()
+ self.runtime.install_apk(self.get_path())
+
+ # TODO
+ @trace
+ def get_supported_targets(self):
+ pass
+
+def install_fdroid_apk(config, apk_name, quiet=False):
+ host = lib.hosts.Host(config, quiet=quiet)
+ device = lib.devices.Device(host, 'i9300')
+ runtime = lib.devices.DeviceRuntime(config, host, device, '6.0', quiet=quiet)
+
+ apk = FDroidAPK(config, host, runtime, apk_name, quiet=quiet)
+ apk.install()
diff --git a/tests/src/lib/heimdall.py b/tests/src/lib/heimdall.py
new file mode 100644
index 0000000..2aaed25
--- /dev/null
+++ b/tests/src/lib/heimdall.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import sh
+import time
+
+from lib.common import get_output
+from lib.common import trace
+
+class Heimdall(object):
+ def __init__(self, host, quiet=False):
+ self.host = host
+ self.quiet = quiet
+
+ # partitions: {'BOOT': '/tmp/tmp.Tq9jPtBpmj/recovery-n7100.img'}
+ @trace
+ def flash(self, partitions, retries=100):
+ command = ['heimdall', 'flash']
+
+ for partition, file_path in partitions.items():
+ command.append('--' + partition)
+ command.append(file_path)
+
+ output = ""
+ while retries > 0:
+ try:
+ output = self.host.run(command)
+ except sh.ErrorReturnCode_1:
+ # Very often, hemdall has error like this one and returns 1:
+ # libusb: error [get_usbfs_fd] libusb couldn't open USB
+ # device /dev/bus/usb/002/009, errno=5
+ # ERROR: Failed to access device. libusb error: -1
+ # In the case above, retrying fixed it. It's probably timing
+ # sensitive somehow.
+ time.sleep(1)
+ retries -= 1
+ break
+
+ return output
+
+ @trace
+ def is_ready(self):
+ try:
+ self.host.run(['heimdall', 'detect'])
+ except sh.ErrorReturnCode_1:
+ return False
+
+ return True
+
+ @trace
+ def reboot(self, retries=100):
+ if environment == 'bootloader':
+ while retries > 0:
+ self.host.run(['heimdall', 'close-pc-screen'])
+ time.sleep(1)
+ retries -= 1
diff --git a/tests/src/lib/hosts.py b/tests/src/lib/hosts.py
new file mode 100644
index 0000000..988ba6e
--- /dev/null
+++ b/tests/src/lib/hosts.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os
+import re
+import sh
+
+from lib.common import trace
+
+class Host(object):
+ def __init__(self, config, quiet=False):
+ self.config = config
+ self.quiet = quiet
+ self.tmpdir = None
+
+ @trace
+ def is_remote(self):
+ if self.config['machine'] == None:
+ return False
+ elif re.match('^ssh://', self.config['machine']):
+ return True
+ else:
+ return False
+
+ @trace
+ def run(self, commands):
+ result = None
+
+ if not self.quiet:
+ print('\tRunning \'{}\''.format(' '.join(commands)))
+
+ if type(commands) == type(list()):
+ commands = ' '.join(commands)
+
+ shell_command = 'cd {} && {}'.format('~', commands.replace('"', '\\"'))
+
+ if self.is_remote():
+ machine = re.sub('^ssh://', '', self.config['machine'])
+ result = sh.ssh(machine, shell_command)
+ else:
+ result = sh.bash('-c', shell_command)
+
+ return result
+
+ @trace
+ def get_output(self, running_command):
+ return re.sub(os.linesep + '$', '', str(running_command))
+
+ @trace
+ def mktemp(self):
+ path = '~/.cache/replicant/tmp.XXXXXXXXXX'
+ if self.tmpdir == None:
+ self.run(['mkdir', '-p', os.path.dirname(path)])
+ self.tmpdir = self.get_output(self.run(['mktemp', '-d', path]))
+ return self.tmpdir
diff --git a/tests/src/lib/releases.py b/tests/src/lib/releases.py
new file mode 100644
index 0000000..c75c1a4
--- /dev/null
+++ b/tests/src/lib/releases.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os
+import re
+import sh
+
+import lib.devices
+import lib.hosts
+import lib.releases
+
+from lib.common import get_output
+from lib.common import trace
+
+class ReplicantRelease(object):
+ def __init__(self, config, major_version, minor_version, host, device,
+ quiet=False):
+ self.config = config
+ self.quiet = quiet
+
+ self.major_version = major_version
+ self.minor_version = minor_version
+ self.host = host
+ self.device = device
+
+ @trace
+ def get_recovery_path(self):
+ release_dir = os.sep.join([
+ self.config['releases_directory'],
+ 'replicant' + '-' + self.major_version,
+ self.minor_version])
+ return release_dir + os.sep + 'recovery-{}.img'.format(self.device)
+
+ @trace
+ def get_zip_path(self):
+ release_dir = os.sep.join([
+ self.config['releases_directory'],
+ 'replicant' + '-' + self.major_version,
+ self.minor_version])
+
+ filename = 'replicant-{}-{}.zip'.format(self.major_version,
+ self.device)
+
+ if self.major_version == '6.0':
+ if self.minor_version > '0003':
+ filename = 'replicant-{}-{}-{}.zip'.format(self.major_version,
+ self.minor_version,
+ self.device)
+
+ return release_dir + os.sep + filename
+
+ @trace
+ def get_supported_targets(self):
+ if self.major_version != '6.0':
+ assert(False)
+
+ return ['espresso3g', 'espressowifi', 'i9100', 'i9300', 'i9305',
+ 'maguro', 'n5100', 'n5110', 'n7000', 'n7100']
+
+class RecoveryRuntime(object):
+ def __init__(self, config, host, device, release, quiet=False):
+ self.config = config
+ self.quiet = quiet
+ self.host = host
+ self.device = device
+ self.release = release
+
+ @trace
+ def get_partition(self, partition):
+ if not self.release.major_version == '6.0':
+ assert(False)
+
+ if str(self.device) in ['i9100', 'i9300', 'n5100']:
+ return '/dev/block/platform/dw_mmc/by-name/' + str(partition)
+ elif str(self.device) in ['maguro']:
+ return '/dev/block/platform/omap/omap_hsmmc.0/by-name/' \
+ + str(partition)
+ elif str(self.device) in ['p3100']:
+ return '/dev/block/platform/omap/omap_hsmmc.1/by-name/' \
+ + str(partition)
+ else:
+ # TODO:
+ # - i9100[OK], i9300[OK], i9305, maguro, n5100[OK], n7000, n7100:
+ # /dev/block/platform/*/by-name/
+ # - i9100, i9300, i9305, n7000, n7100:
+ # /dev/block/platform/*/by-name/
+ # - p3100[OK]: /dev/block/platform/*/*/by-name/EFS
+ # - p5100: ?, p5510: ?, p3110: ?
+ assert(False)
+
+ @trace
+ def get_cache_partition(self):
+ if self.release.major_version != '6.0':
+ # TODO
+ assert(False)
+
+ if str(self.device) in ['maguro']:
+ return self.get_partition('cache')
+ elif str(self.device) in ['espresso3g', 'espressowifi', 'i9100',
+ 'i9300', 'i9305', 'n5100', 'n5110', 'n7000',
+ 'n7100']:
+ return self.get_partition('CACHE')
+
+ @trace
+ def wait_for_ready(self):
+ self.host.run(['adb', 'wait-for-recovery'])
+
+ @trace
+ def is_remote(self):
+ if self.config['machine'] == None:
+ return False
+ if re.match('^ssh://', self.config['machine']):
+ return True
+ else:
+ return False
+
+ @trace
+ def run(self, commands):
+ result = None
+
+ if not self.quiet:
+ print('\tRunning \'{}\''.format(' '.join(commands)))
+
+ if type(commands) == type(list()):
+ commands = ' '.join(commands)
+
+ shell_command = 'cd {} && {}'.format('~',
+ 'adb shell '
+ + '"'
+ + commands.replace('"', '\\"')
+ + '"')
+
+ self.wait_for_ready()
+
+ if self.is_remote():
+ machine = re.sub('^ssh://', '', self.config['machine'])
+ result = sh.ssh(machine, shell_command)
+ else:
+ result = sh.bash('-c', shell_command)
+
+ return result
+
+ @trace
+ def mount_cache(self):
+ return self.run(['mount', self.get_cache_partition(), '/cache'])
+
+ # Documentation: bootable/recovery/recovery.cpp
+ @trace
+ def append_recovery_command(self, command):
+ self.wait_for_ready()
+ self.run(['echo', command, '>>', '/cache/recovery/command'])
+
+ @trace
+ def wipe_cache_and_data(self):
+ if not self.quiet:
+ print('Starting to wipe the data data and cache partition:')
+
+ self.mount_cache()
+
+ self.append_recovery_command('--wipe_data')
+
+ self.host.run(['adb', 'reboot', 'recovery'])
+
+ @trace
+ def install_zip(self, recovery_path):
+ filename = recovery_path.split(os.sep)[-1]
+ if not self.quiet:
+ print('Installing {} on {}'.format(filename, self.device))
+
+ if not self.quiet:
+ print('Starting to install {} on {}'.format(filename, self.device))
+
+ self.append_recovery_command('--sideload')
+ self.host.run(['adb', 'reboot', 'recovery'])
+
+def install_release(local_config, major_version, minor_version, device):
+ if not self.quiet:
+ print('Installing Replicant {} {} on {}'.format(filename,
+ major_version,
+ minor_version,
+ self.device))
+ host = lib.hosts.Host(local_config)
+ device = lib.devices.Device(host, 'i9300')
+
+ release = lib.releases.ReplicantRelease(local_config,
+ major_version, minor_version,
+ host, device)
+
+ runtime = lib.devices.DeviceRuntime(local_config, host, device, release)
+
+ runtime.install_recovery(release.get_recovery_path(), add_root=True)
+ runtime.wipe_cache_and_data()
+ runtime.install_zip(release.get_zip_path(), add_root=True)
+ runtime.boot('replicant')
diff --git a/tests/src/lib/replicant_releases.py b/tests/src/lib/replicant_releases.py
new file mode 100644
index 0000000..3da6e47
--- /dev/null
+++ b/tests/src/lib/replicant_releases.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os
+import re
+import sh
+
+import lib.devices
+import lib.hosts
+
+from lib.common import get_output
+from lib.common import trace
+
+class ReplicantRelease(object):
+ def __init__(self, config, major_version, minor_version, host, device,
+ quiet=False):
+ self.config = config
+ self.quiet = quiet
+
+ self.major_version = major_version
+ self.minor_version = minor_version
+ self.host = host
+ self.device = device
+
+ @trace
+ def get_recovery_path(self):
+ path = ""
+ path += os.sep.join([
+ self.config['releases_directory'],
+ 'replicant' + '-' + self.major_version,
+ self.minor_version])
+ path += os.sep + 'images' + os.sep + str(self.device)
+ path += os.sep + 'recovery-{}.img'.format(self.device)
+ return path
+
+ @trace
+ def get_zip_path(self):
+ filename = 'replicant-{}-{}.zip'.format(self.major_version,
+ self.device)
+ if self.major_version == '6.0':
+ if self.minor_version > '0003':
+ filename = 'replicant-{}-{}-{}.zip'.format(self.major_version,
+ self.minor_version,
+ self.device)
+ path = ""
+ path += os.sep.join([
+ self.config['releases_directory'],
+ 'replicant' + '-' + self.major_version,
+ self.minor_version])
+ path += os.sep + 'images' + os.sep + str(self.device)
+ path += os.sep + filename
+ return path
+
+ @trace
+ def get_supported_targets(self):
+ if self.major_version != '6.0':
+ assert(False)
+
+ return ['espresso3g', 'espressowifi', 'i9100', 'i9300', 'i9305',
+ 'maguro', 'n5100', 'n5110', 'n7000', 'n7100']
+
+class RecoveryRuntime(object):
+ def __init__(self, config, host, device, release, quiet=False):
+ self.config = config
+ self.quiet = quiet
+ self.host = host
+ self.device = device
+ self.release = release
+
+ @trace
+ def get_partition(self, partition):
+ if not self.release.major_version == '6.0':
+ assert(False)
+
+ if str(self.device) in ['i9100', 'i9300', 'n7100', 'n5100']:
+ return '/dev/block/platform/dw_mmc/by-name/' + str(partition)
+ elif str(self.device) in ['maguro']:
+ return '/dev/block/platform/omap/omap_hsmmc.0/by-name/' \
+ + str(partition)
+ elif str(self.device) in ['p3100']:
+ return '/dev/block/platform/omap/omap_hsmmc.1/by-name/' \
+ + str(partition)
+ else:
+ # TODO:
+ # - i9305
+ # - n5110
+ # - n7000
+ # - p3110
+ # - p311
+ # - p5100
+ # - p5510
+ assert(False)
+
+ @trace
+ def get_boot_partition(self):
+ if self.release.major_version != '6.0':
+ # TODO
+ assert(False)
+
+ if str(self.device) in ['i9300', 'i9305', 'n7100']:
+ return self.get_partition('BOOT')
+ else:
+ # TODO:
+ # - i9100
+ # - maguro
+ # - n5110
+ # - n7000
+ # - p3100
+ # - p3110
+ # - p3113
+ # - p5100
+ # - p5100
+ # - p5510
+ assert(False)
+
+ @trace
+ def get_cache_partition(self):
+ if self.release.major_version != '6.0':
+ # TODO
+ assert(False)
+
+ if str(self.device) in ['maguro']:
+ return self.get_partition('cache')
+ elif str(self.device) in ['espresso3g', 'espressowifi', 'i9100',
+ 'i9300', 'i9305', 'n5100', 'n5110', 'n7000',
+ 'n7100']:
+ return self.get_partition('CACHE')
+ else:
+ # TODO:
+ # - p3110
+ # - p3113
+ # - p5100
+ # - p5510
+ assert(False)
+
+ @trace
+ def wait_for_ready(self):
+ self.host.run(['adb', 'wait-for-recovery'])
+
+ @trace
+ def is_remote(self):
+ if self.config['machine'] == None:
+ return False
+ if re.match('^ssh://', self.config['machine']):
+ return True
+ else:
+ return False
+
+ @trace
+ def run(self, commands):
+ result = None
+
+ if not self.quiet:
+ print('\tRunning \'{}\''.format(' '.join(commands)))
+
+ if type(commands) == type(list()):
+ commands = ' '.join(commands)
+
+ shell_command = 'cd {} && {}'.format('~',
+ 'adb shell '
+ + '"'
+ + commands.replace('"', '\\"')
+ + '"')
+
+ self.wait_for_ready()
+
+ if self.is_remote():
+ machine = re.sub('^ssh://', '', self.config['machine'])
+ result = sh.ssh(machine, shell_command)
+ else:
+ result = sh.bash('-c', shell_command)
+
+ return result
+
+ @trace
+ def mount_cache(self):
+ return self.run(['mount', self.get_cache_partition(), '/cache'])
+
+ # Documentation: bootable/recovery/recovery.cpp
+ @trace
+ def append_recovery_command(self, command):
+ self.wait_for_ready()
+ self.run(['echo', command, '>>', '/cache/recovery/command'])
+
+ @trace
+ def wipe_cache_and_data(self):
+ if not self.quiet:
+ print('Starting to wipe the data data and cache partition:')
+
+ self.mount_cache()
+
+ self.append_recovery_command('--wipe_data')
+
+ self.host.run(['adb', 'reboot', 'recovery'])
+
+ @trace
+ def install_zip(self, recovery_path):
+ filename = recovery_path.split(os.sep)[-1]
+
+ if not self.quiet:
+ print('RecoveryRuntime.install_zip: '
+ + 'Starting to install {} on {}'.format(filename, self.device))
+
+ self.append_recovery_command('--sideload')
+ self.host.run(['adb', 'reboot', 'recovery'])
+
+ @trace
+ def install_boot_img(self, path, retries=100):
+ if not self.quiet:
+ print('Starting to install the {} boot image on {}'.format(
+ path, self.device))
+
+ filename = os.path.basename(path)
+ partition = self.get_boot_partition()
+
+ self.host.run(['adb', 'wait-for-recovery'])
+ self.host.run(['adb', 'push', path, '/' + filename])
+ self.host.run(['adb', 'shell', 'dd', 'if=/dev/zero', 'of=' + partition])
+ self.host.run(['adb', 'shell', 'dd', 'if=' + filename, 'of=' + partition])
+ self.host.run(['adb', 'shell', 'sync'])
+ self.host.run(['adb', 'shell', 'rm', '-f', '/' + filename])
+
+def install_replicant_release(local_config,
+ major_version, minor_version,
+ device, wipe_cache_and_data=True,
+ add_root=True,
+ disable_modem=False, quiet=False):
+ if not quiet:
+ print('RecoveryRuntime.install_replicant_release: '
+ + 'Installing Replicant {} release for the {}'.format(
+ minor_version, device))
+ host = lib.hosts.Host(local_config, quiet=quiet)
+ device = lib.devices.Device(host, device, quiet=quiet)
+
+ release = lib.replicant_releases.ReplicantRelease(local_config,
+ major_version,
+ minor_version,
+ host, device, quiet)
+
+ runtime = lib.devices.DeviceRuntime(local_config, host, device, release,
+ quiet=quiet)
+
+ runtime.install_recovery(release.get_recovery_path(), add_root)
+ if wipe_cache_and_data:
+ runtime.wipe_cache_and_data()
+ runtime.install_zip(release.get_zip_path(), add_root, disable_modem)
+ runtime.boot('replicant')
+ runtime.wait_for_boot_complete()
diff --git a/tests/src/lib/replicant_source.py b/tests/src/lib/replicant_source.py
new file mode 100644
index 0000000..80aa581
--- /dev/null
+++ b/tests/src/lib/replicant_source.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os
+import re
+import sh
+
+from lib.common import get_output
+from lib.common import trace
+
+class ReplicantSource(object):
+ def __init__(self, config, quiet=False):
+ self.config = config
+ self.quiet = quiet
+
+ @trace
+ def is_remote(self):
+ if self.config['machine'] == None:
+ return False
+ elif re.match('^ssh://', self.config['machine']):
+ return True
+ else:
+ return False
+
+ @trace
+ def run(self, commands):
+ result = None
+
+ if not self.quiet:
+ print('\tRunning \'{}\''.format(' '.join(commands)))
+
+
+ if type(commands) == type(list()):
+ commands = ' '.join(commands)
+ replicant_dir = self.config['replicant_directory']
+ if not re.match('^/', replicant_dir) and \
+ not re.match('^~', replicant_dir):
+ replicant_dir = '~' + os.sep + replicant_dir
+
+ shell_command = 'cd {} && {}'.format(replicant_dir, commands)
+
+ if self.is_remote():
+ machine = re.sub('^ssh://', '', self.config['machine'])
+ result = sh.ssh(machine, shell_command)
+ else:
+ result = sh.bash('-c', shell_command)
+
+ return result
+
+ @trace
+ def repo_init(self):
+ if not self.quiet:
+ print('Starting to repo init:')
+ for manifest in ['manifest.xml', 'manifests', 'manifests.git']:
+ self.run([
+ 'rm', '-rf', '.repo' + os.sep + manifest
+ ])
+ self.run(
+ [
+ 'repo', 'init',
+ '-u', self.config['manifest_url'],
+ '-b', self.config['manifest_branch']
+ ]
+ )
+
+ @trace
+ def repo_needs_init_again(self):
+ if not self.quiet:
+ print('Starting to check if repo init is needed:')
+ git_cmd = ['git', '-C', '.repo/manifests']
+
+ for manifest in ['manifest.xml', 'manifests', 'manifests.git']:
+ manifest_path = '.repo' + os.sep + manifest
+
+ try:
+ output = get_output(self.run(['ls', '-d', manifest_path]))
+ except sh.ErrorReturnCode_2:
+ return True
+
+ output = get_output(
+ self.run(git_cmd + ['remote', 'get-url', 'origin'])
+ )
+
+ if output != self.config['manifest_url']:
+ return True
+
+ output = get_output(self.run(git_cmd + ['diff']))
+ if len(output) > 0:
+ return True
+
+ # If the manifest was force pushed and that the git pull cannot
+ # be fast-foward, we need to detect that.
+ self.run(git_cmd + ['fetch', 'origin'])
+
+ try:
+ self.run(git_cmd + ['merge-base', '--is-ancestor',
+ 'HEAD',
+ 'origin' + '/'
+ + self.config['manifest_branch']])
+ except sh.ErrorReturnCode_2:
+ return True
+
+ return False
+
+ @trace
+ def sync(self):
+ if not self.quiet:
+ print('Starting repo sync:')
+ self.run(['repo', 'sync', '--force-sync'])
+
+ @trace
+ def build_images(self, device):
+ if not self.quiet:
+ print('Starting build for {}'.format(device))
+ self.run(['./vendor/replicant/build.sh', device])
diff --git a/tests/src/lib/test.py b/tests/src/lib/test.py
new file mode 100644
index 0000000..cd5172d
--- /dev/null
+++ b/tests/src/lib/test.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os
+import re
+import sh
+import time
+
+import lib.devices
+import lib.hosts
+import lib.releases
+
+from lib.common import trace
+from lib.common import get_output
+
+class ReplicantDevRelease(object):
+ def __init__(self, config, major_version, minor_version, host, device,
+ quiet=False):
+ self.config = config
+ self.quiet = quiet
+
+ self.major_version = major_version
+ self.minor_version = minor_version
+ self.host = host
+ self.device = device
+
+ self.dist_dirs = os.sep.join(['out', 'dist', 'i9300'])
+ self.tmpdir = None
+
+ @trace
+ def is_remote(self):
+ if self.config['machine'] == None:
+ return False
+ elif re.match('^ssh://', self.config['machine']):
+ return True
+ else:
+ return False
+
+ @trace
+ def sync(self, remote_path):
+ if not self.is_remote():
+ assert(False)
+
+ if not self.quiet:
+ print('Starting to sync {}:'.format(remote_path))
+
+ if not self.tmpdir:
+ self.tmpdir = get_output(self.host.run(['mktemp', '-d']))
+
+ remote_arg = \
+ re.sub('^ssh://', '', self.config['machine']) + ':' + remote_path
+
+ args = ['-av', '--progress', '--partial', remote_arg, self.tmpdir]
+ if not self.quiet:
+ print('\tRunning \'{}\''.format('rsync' + ' '.join(args)))
+
+ sh.rsync(args)
+
+ @trace
+ def get_path(self, filename):
+ path = self.config['replicant_directory'] \
+ + os.sep \
+ + self.dist_dirs \
+ + os.sep \
+ + filename
+
+ if self.is_remote():
+ self.sync(path)
+ return self.tmpdir + os.sep + os.path.basename(path)
+ else:
+ return path
+
+ @trace
+ def get_recovery_path(self):
+ return self.get_path('recovery-{}.img'.format(self.device))
+
+ @trace
+ def get_zip_path(self):
+ return self.get_path('replicant-{}-{}-{}.zip'.format(
+ self.major_version, self.minor_version, self.device))
+
+ @trace
+ def get_supported_targets(self):
+ if self.major_version != '6.0':
+ assert(False)
+
+ return ['espresso3g', 'espressowifi', 'i9100', 'i9300', 'i9305',
+ 'maguro', 'n5100', 'n5110', 'n7000', 'n7100']
+
+def build_replicant_6(device='i9300'):
+ replicant = ReplicantSource(remote_config)
+ if replicant.repo_needs_init_again():
+ replicant.repo_init()
+ replicant.sync()
+ replicant.build_images(device)
+
+def install_dev_image(device='i9300'):
+ host = lib.hosts.Host(local_config)
+ device = lib.devices.Device(host, 'i9300')
+ release = ReplicantDevRelease(remote_config, '6.0', 'dev', host,
+ device)
+ runtime = lib.devices.DeviceRuntime(local_config, host, device, release)
+
+ runtime.install_recovery(release.get_recovery_path(), add_root=True)
+ runtime.install_zip(release.get_zip_path())