diff --git a/CHANGES.md b/CHANGES.md index 6875a50be2..1d3f93d0a9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,6 +29,7 @@ Unreleased - An empty `envvar`, such as `[]` or `""`, shows the same help and error message as no `envvar` when `show_envvar=True`. The help screen used to show `[env var: ]`. {pr}`3884` +- Document how to build a standalone application with Nuitka. {issue}`2757` {pr}`3897` ## Version 8.5.0 diff --git a/docs/index.md b/docs/index.md index c280b19740..8a603ce20d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -87,7 +87,8 @@ virtualenv entry-points setuptools -standalone-apps +standalone-apps-briefcase +standalone-apps-nuitka upgrade-guides support-multiple-versions ``` diff --git a/docs/standalone-apps.md b/docs/standalone-apps-briefcase.md similarity index 94% rename from docs/standalone-apps.md rename to docs/standalone-apps-briefcase.md index 48b4d39bb5..51394fd87b 100644 --- a/docs/standalone-apps.md +++ b/docs/standalone-apps-briefcase.md @@ -19,7 +19,7 @@ and use ``briefcase --help`` to understand what features are available. Install Briefcase in the virtual environment: -```console +```shell-session pip install briefcase ``` @@ -106,12 +106,12 @@ if __name__ == "__main__": main() ``` -## Running +## Running from the Source Tree -Use ``briefcase dev`` to run the application directly from the source -tree. Pass command line arguments after ``--``: +Use ``briefcase dev`` to start the application from the source tree. +Pass command line arguments after ``--``: -```console +```shell-session $ briefcase dev -- World Hello, World! $ briefcase dev -- World --count 2 @@ -123,7 +123,7 @@ Hello, World! To create a distributable executable, run the following commands: -```console +```shell-session briefcase create briefcase build briefcase package @@ -139,7 +139,7 @@ On macOS, this produces a ``.pkg`` installer. On Windows, it produces a Once installed, users can run the application directly from the terminal: -```console +```shell-session $ hello-cli World Hello, World! $ hello-cli World --count 2 diff --git a/docs/standalone-apps-nuitka.md b/docs/standalone-apps-nuitka.md new file mode 100644 index 0000000000..2aeec40132 --- /dev/null +++ b/docs/standalone-apps-nuitka.md @@ -0,0 +1,208 @@ +# Standalone Application with Nuitka + +[Nuitka](https://nuitka.net/) compiles a Python project into a native +executable. It bundles a Python interpreter and all dependencies, so the +result runs on a machine with no Python installed. + +- Compiles Python to C, then to machine code. +- Produces one executable file, or a folder of files. +- Builds for the operating system and architecture it runs on: macOS, + Windows, and Linux. + +This page outlines the basics of packaging a Click application with +Nuitka. Be sure to read its +[user manual](https://nuitka.net/user-documentation/) and use +``python -m nuitka --help`` to understand what features are available. + +## Installation + +Install Nuitka in the virtual environment: + +```shell-session +$ pip install "nuitka[onefile]" +``` + +The ``onefile`` extra installs the compression library Nuitka uses to pack +the executable into a single file. + +Nuitka compiles to C, so it needs a C compiler too. On macOS, install the +Xcode command line tools with ``xcode-select --install``. On Linux, install +``gcc``. On Windows, Nuitka uses the newest installed Visual Studio +compiler, and it asks to download MinGW64 when it finds none. + +## Configuration + +The build on this page takes every setting from the command line, so +Nuitka needs no configuration file for it. It takes the package to compile +as an argument, and it finds the dependencies in the virtual environment +it runs in. + +Given a Click application with the following project structure: + +```text +hello-cli/ + src/ + hello_cli/ + __init__.py + app.py + LICENSE + pyproject.toml +``` + +Where ``app.py`` contains a Click command: + +```python +import click + + +@click.command() +@click.argument("name", default="World") +@click.option("--count", default=1, help="Number of times to greet.") +def main(name, count): + """Greet someone by NAME (default: World).""" + for _ in range(count): + click.echo(f"Hello, {name}!") +``` + +Declare the dependencies and the command in ``pyproject.toml``, the way +{doc}`entry-points` describes: + +```toml +[project] +name = "hello-cli" +version = "0.0.1" +description = "My first application" +dependencies = [ + "click>=8.1", +] + +[project.scripts] +hello-cli = "hello_cli.app:main" + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" +``` + +Install the project in the virtual environment: + +```shell-session +$ pip install . +``` + +The install gives Nuitka the dependencies to bundle. It also makes the +package metadata available to the build, which +[Package Metadata](#package-metadata) needs. + +## Entry Point + +Nuitka compiles the package's ``__main__.py`` into the executable and runs +it as the main program, so a ``__main__.py`` file **must** exist in the +package. Without it, the build stops with +``can't find '__main__' module``. + +Create ``__main__.py`` in the package directory and call the Click command: + +```python +from hello_cli.app import main + +if __name__ == "__main__": + main() +``` + +## Building + +Compile the package from the project root: + +```shell-session +$ python -m nuitka --mode=onefile --output-dir=dist --output-filename=hello-cli --remove-output src/hello_cli +``` + +The key parts of the command are: + +- ``--mode=onefile``: produce one self-contained executable file. + ``--mode=standalone`` produces a folder with the executable and the + files it needs instead. A onefile executable unpacks itself into a + temporary directory every time it starts, which a standalone one does + not. +- ``--output-dir=dist``: write the build output to ``dist`` instead of the + current directory. +- ``--output-filename=hello-cli``: name the executable. Without it, the + executable is named after the package and gets a ``.bin`` suffix on + Linux and macOS. Windows adds ``.exe`` whether you name the file or + not. A standalone build puts the executable in a folder named after + the package and ending with ``.dist``, whatever name this option + gives. +- ``--remove-output``: delete the intermediate build folders and keep only + the executable. +- ``src/hello_cli``: the package to compile. Pass the directory that holds + ``__main__.py``. Nuitka warns if you pass the file itself. + +Compiling takes longer than running a script, because Nuitka compiles every +module it bundles, Click and the standard library included. The executable +it produces runs with no Python installed, on the operating system and +architecture it was built for. + +Nuitka builds for the operating system it runs on. To publish an executable +for each platform, run the build on each one, which a continuous +integration workflow usually does. + +## Running the Executable + +Run it and pass arguments to it: + +```shell-session +$ dist/hello-cli World +Hello, World! +$ dist/hello-cli World --count 2 +Hello, World! +Hello, World! +``` + +The executable reports its own file name as the program name, so help and +errors read like the ones the installed ``hello-cli`` command produces: + +```shell-session +$ dist/hello-cli --help +Usage: hello-cli [OPTIONS] [NAME] + + Greet someone by NAME (default: World). + +Options: + --count INTEGER Number of times to greet. + --help Show this message and exit. +``` + +## Package Metadata + +{func}`click.version_option` reads the version from the package metadata +with {mod}`importlib.metadata`. A compiled executable carries no package +metadata by default, so the option raises +``RuntimeError: 'hello_cli' is not installed.`` The message also suggests +passing ``package_name`` instead. + +Add ``--include-distribution-metadata=hello-cli`` to the build command, +naming the distribution from ``[project] name``. The option reads the +metadata of an installed distribution, which is what the ``pip install .`` +step above provides. Give the distribution name, not the import name: +``hello-cli``, not ``hello_cli``. Nuitka resolves ``hello_cli`` too, then +warns that the distribution is really named ``hello-cli``. + +Pass ``version`` to the decorator instead, if you would rather not bundle +the metadata: + +```python +@click.version_option(version="0.0.1") +``` + +## Lazily Loaded Subcommands + +Nuitka follows the imports it finds in the source. A group that loads its +subcommands at run time, like the one in +[Lazily Loading Subcommands](complex.md#lazily-loading-subcommands), names +them as strings, so Nuitka leaves those modules out of the executable. The +subcommand then fails with ``ModuleNotFoundError``. + +Add the package that holds the subcommands to the build command, with +``--include-package=hello_cli.commands``. ``--include-package`` adds every +module below a package, and ``--include-module`` adds a single module.