aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside-tools/deploy_lib/android/android_helper.py
blob: 7d2f5d57523d556e4b732c83f0ea0fa92348242a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

import logging
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import List, Set
from zipfile import ZipFile

from jinja2 import Environment, FileSystemLoader

from .. import run_command


@dataclass
class AndroidData:
    """
    Dataclass to store all the Android data obtained through cli
    """
    wheel_pyside: Path
    wheel_shiboken: Path
    ndk_path: Path
    sdk_path: Path


def create_recipe(version: str, component: str, wheel_path: str, generated_files_path: Path,
                  qt_modules: List[str] = None, local_libs: List[str] = None,
                  plugins: List[str] = None):
    '''
    Create python_for_android recipe for PySide6 and shiboken6
    '''
    qt_plugins = []
    if plugins:
        # split plugins based on category
        for plugin in plugins:
            plugin_category, plugin_name = plugin.split('_', 1)
            qt_plugins.append((plugin_category, plugin_name))

    qt_local_libs = []
    if local_libs:
        qt_local_libs = [local_lib for local_lib in local_libs if local_lib.startswith("Qt6")]

    rcp_tmpl_path = Path(__file__).parent / "recipes" / f"{component}"
    environment = Environment(loader=FileSystemLoader(rcp_tmpl_path))
    template = environment.get_template("__init__.tmpl.py")
    content = template.render(
        version=version,
        wheel_path=wheel_path,
        qt_modules=qt_modules,
        qt_local_libs=qt_local_libs,
        qt_plugins=qt_plugins
    )

    recipe_path = generated_files_path / "recipes" / f"{component}"
    recipe_path.mkdir(parents=True, exist_ok=True)
    logging.info(f"[DEPLOY] Writing {component} recipe into {str(recipe_path)}")
    with open(recipe_path / "__init__.py", mode="w", encoding="utf-8") as recipe:
        recipe.write(content)


def extract_and_copy_jar(wheel_path: Path, generated_files_path: Path) -> str:
    '''
    extracts the PySide6 wheel and copies the 'jar' folder to 'generated_files_path'.
    These .jar files are added to the buildozer.spec file to be used later by buildozer
    '''
    jar_path = generated_files_path / "jar"
    jar_path.mkdir(parents=True, exist_ok=True)
    archive = ZipFile(wheel_path)
    jar_files = [file for file in archive.namelist() if file.startswith("PySide6/jar")]
    for file in jar_files:
        archive.extract(file, jar_path)
    return (jar_path / "PySide6" / "jar").resolve() if jar_files else None


def get_wheel_android_arch(wheel: Path):
    '''
    Get android architecture from wheel
    '''
    supported_archs = ["aarch64", "armv7a", "i686", "x86_64"]
    for arch in supported_archs:
        if arch in wheel.stem:
            return arch

    return None


def get_llvm_readobj(ndk_path: Path) -> Path:
    '''
    Return the path to llvm_readobj from the Android Ndk
    '''
    # TODO: Requires change if Windows platform supports Android Deployment or if we
    # support host other than linux-x86_64
    return (ndk_path / "toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readobj")


def find_lib_dependencies(llvm_readobj: Path, lib_path: Path, used_dependencies: Set[str] = None,
                          dry_run: bool = False):
    """
    Find all the Qt dependencies of a library using llvm_readobj
    """
    if lib_path.name in used_dependencies:
        return

    used_dependencies.add(lib_path.name)

    command = [str(llvm_readobj), "--needed-libs", str(lib_path)]

    # even if dry_run is given, we need to run the actual command to see all the dependencies
    # for which llvm-readelf is run.
    if dry_run:
        _, output = run_command(command=command, dry_run=dry_run, fetch_output=True)
    _, output = run_command(command=command, dry_run=False, fetch_output=True)

    dependencies = set()
    neededlibraries_found = False
    for line in output.splitlines():
        line = line.decode("utf-8").lstrip()
        if line.startswith("NeededLibraries") and not neededlibraries_found:
            neededlibraries_found = True
        if neededlibraries_found and line.startswith("libQt"):
            dependencies.add(line)
            used_dependencies.add(line)
            dependent_lib_path = lib_path.parent / line
            find_lib_dependencies(llvm_readobj, dependent_lib_path, used_dependencies, dry_run)

    if dependencies:
        logging.info(f"[DEPLOY] Following dependencies found for {lib_path.stem}: {dependencies}")
    else:
        logging.info(f"[DEPLOY] No Qt dependencies found for {lib_path.stem}")


def find_qtlibs_in_wheel(wheel_pyside: Path):
    """
    Find the path to Qt/lib folder inside the wheel.
    """
    archive = ZipFile(wheel_pyside)
    qt_libs_path = wheel_pyside / "PySide6/Qt/lib"
    qt_libs_path = zipfile.Path(archive, at=qt_libs_path)
    if not qt_libs_path.exists():
        for file in archive.namelist():
            # the dependency files are inside the libs folder
            if file.endswith("android-dependencies.xml"):
                qt_libs_path = zipfile.Path(archive, at=file).parent
                # all dependency files are in the same path
                break

    if not qt_libs_path:
        raise FileNotFoundError("[DEPLOY] Unable to find Qt libs folder inside the wheel")

    return qt_libs_path