aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside-tools/deploy.py
blob: aa03d13d07389deb996c9b8136e034db59fbb166 (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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# Copyright (C) 2022 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

""" pyside6-deploy deployment tool

    Deployment tool that uses Nuitka to deploy PySide6 applications to various desktop (Windows,
    Linux, macOS) platforms.

    How does it work?

    Command: pyside6-deploy path/to/main_file
             pyside6-deploy (incase main file is called main.py)
             pyside6-deploy -c /path/to/config_file

    Platforms supported: Linux, Windows, macOS
    Module binary inclusion:
        1. for non-QML cases, only required modules are included
        2. for QML cases, all modules are included because of all QML plugins getting included
            with nuitka

    Config file:
        On the first run of the tool, it creates a config file called pysidedeploy.spec which
        controls the various characteristic of the deployment. Users can simply change the value
        in this config file to achieve different properties ie. change the application name,
        deployment platform etc.

        Note: This file is used by both pyside6-deploy and pyside6-android-deploy

"""

import argparse
import logging
import traceback
from pathlib import Path
from textwrap import dedent

from deploy_lib import (MAJOR_VERSION, DesktopConfig, cleanup, config_option_exists,
                        finalize, create_config_file, PythonExecutable, Nuitka,
                        HELP_EXTRA_MODULES, HELP_EXTRA_IGNORE_DIRS)


TOOL_DESCRIPTION = dedent(f"""
                          This tool deploys PySide{MAJOR_VERSION} to desktop (Windows, Linux,
                          macOS) platforms. The following types of executables are produced as per
                          the platform:

                          Windows = .exe
                          macOS = .app
                          Linux = .bin
                          """)


def main(main_file: Path = None, name: str = None, config_file: Path = None, init: bool = False,
         loglevel=logging.WARNING, dry_run: bool = False, keep_deployment_files: bool = False,
         force: bool = False, extra_ignore_dirs: str = None, extra_modules_grouped: str = None):

    logging.basicConfig(level=loglevel)
    if config_file and not config_file.exists() and not main_file.exists():
        raise RuntimeError(dedent("""
            Directory does not contain main.py file.
            Please specify the main python entrypoint file or the config file.
            Run "pyside6-deploy desktop --help" to see info about cli options.

            pyside6-deploy exiting..."""))

    # Nuitka command to run
    command_str = None
    config = None
    logging.info("[DEPLOY] Start")

    if extra_ignore_dirs:
        extra_ignore_dirs = extra_ignore_dirs.split(",")

    extra_modules = []
    if extra_modules_grouped:
        tmp_extra_modules = extra_modules_grouped.split(",")
        for extra_module in tmp_extra_modules:
            if extra_module.startswith("Qt"):
                extra_modules.append(extra_module[2:])
            else:
                extra_modules.append(extra_module)

    python = PythonExecutable(dry_run=dry_run, init=init, force=force)
    config_file_exists = config_file and Path(config_file).exists()

    if config_file_exists:
        logging.info(f"[DEPLOY] Using existing config file {config_file}")
    else:
        config_file = create_config_file(dry_run=dry_run, config_file=config_file,
                                         main_file=main_file)

    config = DesktopConfig(config_file=config_file, source_file=main_file, python_exe=python.exe,
                           dry_run=dry_run, existing_config_file=config_file_exists,
                           extra_ignore_dirs=extra_ignore_dirs)

    # set application name
    if name:
        config.title = name

    cleanup(config=config)

    python.install_dependencies(config=config, packages="packages")

    # required by Nuitka for pyenv Python
    add_arg = " --static-libpython=no"
    if python.is_pyenv_python() and add_arg not in config.extra_args:
        config.extra_args += add_arg

    config.modules += list(set(extra_modules).difference(set(config.modules)))

    # writing config file
    # in the case of --dry-run, we use default.spec as reference. Do not save the changes
    # for --dry-run
    if not dry_run:
        config.update_config()

    if config.qml_files:
        logging.info(f"[DEPLOY] Included QML files: {config.qml_files}")

    if init:
        # config file created above. Exiting.
        logging.info(f"[DEPLOY]: Config file {config.config_file} created")
        return

    try:
        # create executable
        if not dry_run:
            logging.info("[DEPLOY] Deploying application")

        nuitka = Nuitka(nuitka=[python.exe, "-m", "nuitka"])
        command_str = nuitka.create_executable(source_file=config.source_file,
                                               extra_args=config.extra_args,
                                               qml_files=config.qml_files,
                                               qt_plugins=config.qt_plugins,
                                               excluded_qml_plugins=config.excluded_qml_plugins,
                                               icon=config.icon,
                                               dry_run=dry_run,
                                               permissions=config.permissions)
    except Exception:
        print(f"[DEPLOY] Exception occurred: {traceback.format_exc()}")
    finally:
        if config.generated_files_path and config:
            finalize(config=config)
            if not keep_deployment_files:
                cleanup(config=config)

    logging.info("[DEPLOY] End")
    return command_str


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=TOOL_DESCRIPTION)

    parser.add_argument("-c", "--config-file", type=lambda p: Path(p).absolute(),
                        default=(Path.cwd() / "pysidedeploy.spec"),
                        help="Path to the .spec config file")

    parser.add_argument(
        type=lambda p: Path(p).absolute(),
        help="Path to main python file", nargs="?", dest="main_file",
        default=None if config_option_exists() else Path.cwd() / "main.py")

    parser.add_argument(
        "--init", action="store_true",
        help="Create pysidedeploy.spec file, if it doesn't already exists")

    parser.add_argument(
        "-v", "--verbose", help="Run in verbose mode", action="store_const",
        dest="loglevel", const=logging.INFO)

    parser.add_argument("--dry-run", action="store_true", help="Show the commands to be run")

    parser.add_argument(
        "--keep-deployment-files", action="store_true",
        help="Keep the generated deployment files generated")

    parser.add_argument("-f", "--force", action="store_true", help="Force all input prompts")

    parser.add_argument("--name", type=str, help="Application name")

    parser.add_argument("--extra-ignore-dirs", type=str, help=HELP_EXTRA_IGNORE_DIRS)

    parser.add_argument("--extra-modules", type=str, help=HELP_EXTRA_MODULES)

    args = parser.parse_args()

    main(args.main_file, args.name, args.config_file, args.init, args.loglevel, args.dry_run,
         args.keep_deployment_files, args.force, args.extra_ignore_dirs, args.extra_modules)