diff options
Diffstat (limited to 'tests/src')
-rwxr-xr-x | tests/src/install-replicant-6.0-0003.py | 50 | ||||
-rw-r--r-- | tests/src/lib/__init__.py | 0 | ||||
-rw-r--r-- | tests/src/lib/bootloaders.py | 201 | ||||
-rw-r--r-- | tests/src/lib/common.py | 39 | ||||
-rw-r--r-- | tests/src/lib/config.py | 92 | ||||
-rw-r--r-- | tests/src/lib/devices.py | 207 | ||||
-rw-r--r-- | tests/src/lib/fdroid.py | 60 | ||||
-rw-r--r-- | tests/src/lib/heimdall.py | 68 | ||||
-rw-r--r-- | tests/src/lib/hosts.py | 67 | ||||
-rw-r--r-- | tests/src/lib/releases.py | 206 | ||||
-rw-r--r-- | tests/src/lib/replicant_releases.py | 260 | ||||
-rw-r--r-- | tests/src/lib/replicant_source.py | 127 | ||||
-rw-r--r-- | tests/src/lib/test.py | 117 | ||||
-rw-r--r-- | tests/src/replicant_tests.conf | 24 | ||||
-rwxr-xr-x | tests/src/test-replicant-6.0-0004-rc5-migration.py | 88 |
15 files changed, 1606 insertions, 0 deletions
diff --git a/tests/src/install-replicant-6.0-0003.py b/tests/src/install-replicant-6.0-0003.py new file mode 100755 index 0000000..3649d47 --- /dev/null +++ b/tests/src/install-replicant-6.0-0003.py @@ -0,0 +1,50 @@ +#!/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, sys + +from lib.common import enable_trace +# enable_trace(True) + +from lib.config import ReplicantConfig +from lib.replicant_releases import * + +def usage(progname): + print('Usage: {} <device>'.format(progname)) + print() + print('Example: {} i9300'.format(progname)) + sys.exit(1) + +if __name__ == '__main__': + if len(sys.argv) != 2: + usage(sys.argv[0]) + + device = sys.argv[1] + + config = ReplicantConfig() + + major_version = '6.0' + minor_version = '0003' + + install_replicant_release(config.get('replicant-installer'), + major_version, + minor_version, + device, + wipe_cache_and_data=False, + disable_modem=False, + quiet=True) + + print("[ OK ] Installed Replicant {} {}".format(major_version, + minor_version)) 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()) diff --git a/tests/src/replicant_tests.conf b/tests/src/replicant_tests.conf new file mode 100644 index 0000000..8df8381 --- /dev/null +++ b/tests/src/replicant_tests.conf @@ -0,0 +1,24 @@ +[DEFAULT] + +[replicant-builder] +machine = ssh://lxc-f2a85m-pro-debian-local +replicant_version = 6.0 +replicant_directory = replicant-6.0 +releases_directory = None +manifest_url = git://git.replicant.us/GNUtoo/manifest.git +manifest_branch = replicant-6.0-dev + +[replicant-installer] +machine = None +replicant_version = 6.0 +replicant_directory = ~/work/reference/replicant/replicant-6.0 +releases_directory = ~/work/releases/replicant/images +manifest_url = git://git.replicant.us/GNUtoo/manifest.git +manifest_branch = replicant-6.0-dev + +[fdroid-installer] +machine = None +replicant_version = 6.0 +releases_directory = ~/work/releases/f-droid +manifest_url = git://git.replicant.us/GNUtoo/manifest.git +manifest_branch = replicant-6.0-dev diff --git a/tests/src/test-replicant-6.0-0004-rc5-migration.py b/tests/src/test-replicant-6.0-0004-rc5-migration.py new file mode 100755 index 0000000..c9f0aac --- /dev/null +++ b/tests/src/test-replicant-6.0-0004-rc5-migration.py @@ -0,0 +1,88 @@ +#!/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, sys + +from lib.common import enable_trace +# enable_trace(True) + +from lib.config import ReplicantConfig +from lib.replicant_releases import * +from lib.fdroid import * + +config = ReplicantConfig() + +def usage(progname): + print('Usage: {} <device>'.format(progname)) + print() + print('Example: {} i9300'.format(progname)) + sys.exit(1) + +if __name__ == '__main__': + if len(sys.argv) != 2: + usage(sys.argv[0]) + + device = sys.argv[1] + + # Install Replicant 6.0 0003 with some APKs + apks = ['ch.blinkenlights.android.vanilla_10850.apk', + 'com.termoneplus_351.apk', + 'org.pocketworkstation.pckeyboard_1041001.apk', + 'org.smssecure.smssecure_145.apk'] + + major_version = '6.0' + minor_version = '0003' + + install_replicant_release(config.get('replicant-installer'), + major_version, + minor_version, + device, + wipe_cache_and_data=True, + disable_modem=False, + quiet=True) + + print("[ OK ] Installed Replicant {} {}".format(major_version, + minor_version)) + + for apk in apks: + install_fdroid_apk(config.get('fdroid-installer'), apk, quiet=True) + + print("[ OK ] Installed APKs on top of Replicant {} {}".format( + major_version, minor_version)) + + # Install Replicant 6.0 0004 RC5 + major_version = '6.0' + minor_version = '0004-rc5-transition' + install_replicant_release(config.get('replicant-installer'), + major_version, + minor_version, + device, + wipe_cache_and_data=False, + disable_modem=False, + quiet=True) + print("[ OK ] Installed Replicant {} {}".format(major_version, + minor_version)) + + major_version = '6.0' + minor_version = '0004-rc5' + install_replicant_release(config.get('replicant-installer'), + major_version, + minor_version, + device, + wipe_cache_and_data=False, + disable_modem=False, + quiet=True) + print("[ OK ] Installed Replicant {} {}".format(major_version, + minor_version)) |