Metadata-Version: 2.4
Name: pyship
Version: 0.7.1
Summary: freezer, installer and updater for Python applications
Author-email: abel <j@abel.co>
License-Expression: MIT
Project-URL: Homepage, https://github.com/jamesabel/pyship
Project-URL: Download, https://github.com/jamesabel/pyship
Keywords: freezer,installer,ship
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: setuptools
Requires-Dist: wheel
Requires-Dist: ismain
Requires-Dist: balsa
Requires-Dist: requests
Requires-Dist: attrs
Requires-Dist: typeguard
Requires-Dist: toml
Requires-Dist: semver
Requires-Dist: python-dateutil
Requires-Dist: wheel-inspect
Requires-Dist: boto3
Requires-Dist: awsimple
Requires-Dist: platformdirs
Requires-Dist: pyshipupdate
Dynamic: license-file

# PyShip

[![CI](https://github.com/jamesabel/pyship/actions/workflows/ci.yml/badge.svg)](https://github.com/jamesabel/pyship/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/jamesabel/pyship/branch/main/graph/badge.svg)](https://codecov.io/gh/jamesabel/pyship)

Enables shipping a python application to end users.

## PyShip's Major Features

* Freeze practically any Python application.
* Optional Code Signing (avoid Windows "Unknown Publisher" warning). This is not required, and you can ship your
  application for free, but code signing is supported if you have a Code Signing certificate (typically an additional
  cost).
* Installs via Microsoft Windows App Store or self-hosted installers entirely outside the app store.
* Automatic application updating in the background (no user intervention).
* OS native application (e.g., .exe for Windows).
* Run on OS startup option.

Currently only for Windows. May be extended to other Operating Systems in the future.

## Configuration

pyship settings are configured in your project's `pyproject.toml` under `[tool.pyship]`.

### pyproject.toml

```toml
[tool.pyship]
ui = "cli"               # "cli" (default), "tui", or "gui"
run_on_startup = false   # true to run the app on OS startup (default: false)
code_sign = false        # true to enable code signing (default: false)
```

#### UI Modes

| Value   | Description                                                                                                  | Python interpreter |
|---------|--------------------------------------------------------------------------------------------------------------|--------------------|
| `"cli"` | Standard command-line application. Output streams directly to the console.                                   | `python.exe`       |
| `"tui"` | Text User Interface (e.g. textual, curses, prompt_toolkit). Unbuffered I/O with direct console access.       | `python.exe`       |
| `"gui"` | Graphical application (e.g. PyQt, tkinter). Runs without a console window; output captured for diagnostics.  | `pythonw.exe`      |

> **Legacy**: `is_gui = true` is still accepted and maps to `ui = "gui"`, but emits a deprecation warning.

### AWS Keys in CLI Arguments

The user may optionally provide their AWS keys as CLI arguments. They may also be provided in the `~/.aws/credentials`
file (typical for AWS CLI and boto3). These are not in `pyproject.toml` since it is usually checked into the code repo.

| Argument         | Description           |
|------------------|-----------------------|
| `-i`, `--id`     | AWS Access Key ID     |
| `-s`, `--secret` | AWS Secret Access Key |

## Microsoft Windows Code Signing (Optional)

Signing your executables suppresses the Windows SmartScreen "Unknown Publisher" warning. pyship signs two files: the
launcher stub (`{app_name}.exe`) and the NSIS installer (`{app_name}_installer_*.exe`). The launcher is signed before
NSIS packages it, so the signed binary ends up inside the installer.

pyship supports two signing modes:

- **PFX file** — a `.pfx` certificate file with a password (traditional software certificates)
- **Hardware token** — a USB security key (e.g. Sectigo OV, SafeNet eToken, YubiKey) whose certificate is in the Windows Certificate Store

You need signtool.exe from the Windows SDK for either mode.

While Code Signing is fairly common, it does involve a few steps. The instructions below should help, and pyship 
will try to help you through the process by flagging any issues it sees.

***Warning***: Accessing a computer over RDP (Remote Desktop) usually **does not work** for entering in the 
certificate "PIN". It will likely fail and the error messages will appear as if the PIN you entered is incorrect. 
This is a major issue since most token devices allow only a small number of failed attempts (typically 3 to 5) 
before the device is locked. If you have a token device that requires a PIN, ensure you are directly accessing 
the computer via regular keyboard, mouse, and video.

### Getting a code-signing certificate

An Authenticode certificate identifies you (or your organization) as the publisher of the software. Windows uses it to
suppress SmartScreen warnings and to display your name in UAC prompts and Add/Remove Programs.

**Certificate types**

| Type                         | SmartScreen behaviour                                                                             | Typical cost | Notes                                                                                                                                            |
|------------------------------|---------------------------------------------------------------------------------------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| OV (Organisation Validation) | Builds reputation over time; new certs still trigger SmartScreen until enough installs accumulate | ~$200–400/yr | Issued to a verified company or individual. Most common for open-source and small commercial projects.                                           |
| EV (Extended Validation)     | Immediate SmartScreen reputation from the first install                                           | ~$400–600/yr | **ONLY** for registered organizations (Corporations). NOT for individuals. Requires a hardware token (USB) or cloud HSM. Strongest trust signal. |

**Where to buy**

***tldr:***

Individuals, ~$300-400/year: [Sectigo Code Signing Certificate](https://www.ssl2buy.com/sectigo-code-signing-certificate.php) .

Existing organizations (registered LLCs, Corporations, etc.), ~$400-500/year:
[Sectigo EV Code Signing Certificate](https://www.ssl2buy.com/sectigo-ev-code-signing.php) .

***Other options***

Certificates are issued by Certificate Authorities (CAs). Common options include:

- **SSL.com** — offers OV and EV code-signing certificates; EV certificates can be stored on their cloud-based eSigner
  HSM, making them usable in CI without a physical USB token.
- **DigiCert** — popular for EV certificates; offers KeyLocker (cloud HSM) for CI integration.
- **Sectigo (formerly Comodo)** — widely used OV and EV code-signing certificates.
- **SignPath** — free certificates for open-source projects via their Foundation programme.

The purchase process typically involves:

1. Choose a CA and certificate type (OV or EV).
2. Complete identity verification (business or personal registration documents for OV; additional legal vetting for EV).
3. The CA issues a `.pfx` (PKCS #12) file containing your private key and certificate chain, or provides access via a
   hardware token / cloud HSM.

Verification usually takes 1–5 business days for OV, and 1–2 weeks for EV.

### The PFX file and password

The `.pfx` file (also called `.p12`) is an encrypted archive that bundles your private signing key with the certificate.
The **PFX password** (also called the export password) protects this file — anyone with both the file and the password
can sign software as you.

**Creating a PFX from separate files**

If your CA provided a `.crt` certificate and a `.key` private key separately (common with some CAs), combine them into
a PFX:

```batch
openssl pkcs12 -export -out certificate.pfx -inkey private.key -in certificate.crt -certfile ca_chain.crt
```

OpenSSL will prompt you to set a password — this becomes your PFX password.

**Extracting certificate info from a PFX**

To view the certificate subject (needed for `msix_publisher`):

```batch
certutil -dump certificate.pfx
```

**Security best practices**

- Never commit the `.pfx` file to your source repository. Add `*.pfx` to `.gitignore`.
- Store the PFX password in a secrets manager or CI secrets — not in source code, environment files, or scripts.
- For CI, base64-encode the PFX file and store it as a CI secret, then decode it at build time:
  ```yaml
  - name: Decode signing certificate
    run: echo "${{ secrets.PFX_BASE64 }}" | base64 --decode > certificate.pfx
  - name: Ship
    env:
      PYSHIP_SIGNING_CERTIFICATE_PIN: ${{ secrets.PFX_PASSWORD }}
    run: python ship.py
  ```
- Rotate certificates before expiry. Most code-signing certificates are valid for 1–3 years.

### Getting signtool.exe

Install the [Windows SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/) and select **Windows SDK
Signing Tools for Desktop Apps**. pyship auto-discovers the newest version under
`C:\Program Files (x86)\Windows Kits\10\bin\`.

### Enabling code signing

Set `code_sign=True` to enable signing. pyship runs a pre-flight check (verifies the PFX file exists or the
hardware token is plugged in) and raises `PyshipSigningUnavailable` if signing infrastructure is missing or if
any file fails to sign. When `code_sign=False` (the default), signing is skipped entirely.

### PFX basic usage

```python
from pathlib import Path
from pyship import PyShip

ps = PyShip(
    project_dir=Path("path/to/your/app"),
    code_sign=True,
    pfx_path=Path("path/to/certificate.pfx"),
    certificate_password="your-pfx-password",
)
ps.ship()
```

### CI / environment variable password

Avoid storing the PFX password in source code. Set the `PYSHIP_SIGNING_CERTIFICATE_PIN` environment variable instead —
pyship reads it automatically when `certificate_password` is not set:

```python
ps = PyShip(
    project_dir=Path("path/to/your/app"),
    code_sign=True,
    pfx_path=Path("path/to/certificate.pfx"),
    # password read from PYSHIP_SIGNING_CERTIFICATE_PIN env var at ship() time
)
ps.ship()
```

Set the secret in your CI system (e.g. GitHub Actions → Settings → Secrets) and expose it in your workflow:

```yaml
- name: Ship
  env:
    PYSHIP_SIGNING_CERTIFICATE_PIN: ${{ secrets.PFX_PASSWORD }}
  run: python ship.py
```

### Explicit signtool path

If signtool.exe is not in the default Windows SDK location, point pyship directly at it:

```python
ps = PyShip(
    ...
    signtool_path=Path(r"C:\custom\path\signtool.exe"),
)
```

### Timestamp server

By default pyship uses DigiCert's RFC 3161 server (`http://timestamp.digicert.com`). Override it with the
`timestamp_url` field:

```python
ps = PyShip(
    ...
    timestamp_url="http://timestamp.sectigo.com",
)
```

### Verifying a signed executable

```batch
signtool verify /pa /v YourApp.exe
```

### Skipping signing

Leave `code_sign` as `False` (the default). pyship will build and package the executables without signing them.

### Hardware Token Signing

If your code-signing certificate lives on a USB hardware token (smart card), pyship can sign directly from the
Windows Certificate Store instead of a PFX file. This is common for EV certificates and newer OV certificates
from CAs like Sectigo, DigiCert, and SSL.com.

When the token is plugged in and its middleware is installed, the certificate appears in the Windows Certificate
Store. pyship performs a pre-flight check — it verifies the hardware is present (via Windows PnP device enumeration)
and that the certificate is in the store before attempting to sign.

#### Selecting the certificate

Provide exactly one of these to identify which certificate to use:

| Parameter               | signtool flag | Description                                      |
|-------------------------|---------------|--------------------------------------------------|
| `certificate_sha1`      | `/sha1`       | SHA1 thumbprint (40 hex chars) — most precise     |
| `certificate_subject`   | `/n`          | Certificate subject name — matches by name        |
| `certificate_auto_select` | `/a`        | Auto-select the best signing certificate          |

**Finding the SHA1 thumbprint**

```batch
certutil -user -store My
```

Look for the `Cert Hash(sha1):` line of your code-signing certificate and copy the 40-character hex value.

#### Basic hardware token usage

```python
from pathlib import Path
from pyship import PyShip

ps = PyShip(
    project_dir=Path("path/to/your/app"),
    code_sign=True,
    certificate_sha1="AABBCCDD1122334455667788AABBCCDD11223344",
)
ps.ship()
```

The token's middleware will prompt for the PIN interactively via its own dialog.

#### Providing the token PIN for automation

For CI or unattended builds, set the `PYSHIP_SIGNING_CERTIFICATE_PIN` environment variable. pyship reads it
automatically:

```yaml
# GitHub Actions example
- name: Ship
  env:
    PYSHIP_SIGNING_CERTIFICATE_PIN: ${{ secrets.TOKEN_PIN }}
  run: python ship.py
```

Or pass it directly (not recommended for CI):

```python
ps = PyShip(
    code_sign=True,
    certificate_sha1="AABBCCDD1122334455667788AABBCCDD11223344",
    certificate_password="123456",  # PIN directly
)
```

#### Advanced: CSP and key container

Some tokens (e.g. SafeNet eToken) require specifying the Cryptographic Service Provider:

```python
ps = PyShip(
    code_sign=True,
    certificate_sha1="AABBCCDD1122334455667788AABBCCDD11223344",
    certificate_csp="eToken Base Cryptographic Provider",
    certificate_key_container="my-container",
)
```

## Microsoft Store Distribution (MSIX) (Optional)

The Microsoft Store is an alternative distribution channel that gives users a trusted, one-click install experience and
eliminates SmartScreen warnings entirely. pyship can produce both a traditional NSIS installer **and** an MSIX package
in a single `ship()` call.

### Prerequisites

| Requirement                      | Notes                                                                                                                                                                                                                                                                               |
|----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Windows SDK                      | Provides `makeappx.exe` and `signtool.exe`. Install from [developer.microsoft.com/windows/downloads/windows-sdk](https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/) and select **Windows SDK Signing Tools for Desktop Apps**. pyship auto-discovers both tools. |
| Authenticode certificate         | Same PFX used for code signing. The `Publisher` field in the MSIX manifest must exactly match the certificate subject. Self-signed certificates work for sideloading and testing but not for Store submission.                                                                      |
| Microsoft Partner Center account | Required for Store submission only. Register at [partner.microsoft.com](https://partner.microsoft.com/). One-time $19 fee for individuals.                                                                                                                                          |

### Basic usage

Set `msix=True` and supply the certificate subject DN as `msix_publisher`:

```python
from pathlib import Path
from pyship import PyShip

ps = PyShip(
    project_dir=Path("path/to/your/app"),
    code_sign=True,
    pfx_path=Path("certificate.pfx"),
    # password read from PYSHIP_SIGNING_CERTIFICATE_PIN env var
    msix=True,
    msix_publisher="CN=My Company, O=My Company LLC, C=US",  # must match cert subject exactly
)
ps.ship()
```

This produces two files in `installers/`:

- `{app_name}_installer_win.exe` — the NSIS installer (direct distribution, auto-updates via pyshipupdate)
- `{app_name}_installer_win.msix` — the MSIX package (Store or sideloading, updates managed by the Store)

Both are signed with the same certificate. The NSIS installer is built first; the MSIX is packed from the same app
directory afterwards, so they do not interfere with each other.

### Finding the msix_publisher string

`msix_publisher` must be the exact Distinguished Name of your signing certificate's subject. To read it from your PFX:

```batch
certutil -dump certificate.pfx
```

Look for the `Subject:` line, e.g. `CN=My Company, O=My Company LLC, C=US`. Copy it verbatim — any difference will cause
installation to fail.

### Store logo assets

MSIX packages require three PNG logo files. pyship generates 1×1 white placeholder PNGs automatically, which is
sufficient for testing and sideloading. For Store submission, provide real assets at the correct sizes:

| File                    | Size    |
|-------------------------|---------|
| `StoreLogo.png`         | 50×50   |
| `Square44x44Logo.png`   | 44×44   |
| `Square150x150Logo.png` | 150×150 |

Point pyship at a directory containing these files with `store_assets_dir`:

```python
ps = PyShip(
    ...
    msix=True,
    msix_publisher="CN=My Company, O=My Company LLC, C=US",
    store_assets_dir=Path("store_assets"),
)
```

Any asset not found in that directory falls back to the placeholder PNG automatically.

### Explicit makeappx path

If `makeappx.exe` is not in the default Windows SDK location:

```python
ps = PyShip(
    ...
    makeappx_path=Path(r"C:\custom\path\makeappx.exe"),
)
```

### Sideloading (without the Store)

Signed MSIX packages can be installed directly without going through the Store — double-click the `.msix` file or use:

```batch
Add-AppxPackage -Path "{app_name}_installer_win.msix"
```

This is useful for enterprise deployments and beta distribution.

### Submitting to the Microsoft Store

1. Log in to [Partner Center](https://partner.microsoft.com/dashboard).
2. Go to **Windows & Xbox** → **Overview** → **Create a new app** and reserve your app name.
3. Under **Submissions** → **New submission**, complete the store listing (description, screenshots, age rating,
   pricing).
4. Upload your signed `.msix` from the `installers/` directory under **Packages**.
5. Submit for certification. Review typically takes 1–3 business days.

### Limitations

- `run_on_startup = true` is not supported in MSIX builds. The MSIX runtime requires a `StartupTask` extension in the
  manifest; pyship logs a warning and continues, but the app will not auto-start. Configure startup behaviour manually
  in a custom manifest if needed.
- MSIX packages installed via the Store are updated by the Store, not by pyshipupdate. The self-update logic in
  pyshipupdate is silently inactive when running inside an MSIX container.

### NSIS vs. MSIX comparison

|                 | NSIS installer          | MSIX                           |
|-----------------|-------------------------|--------------------------------|
| Distribution    | Your own URL / S3       | Microsoft Store or sideload    |
| SmartScreen     | Suppressed with EV cert | Not shown (implicitly trusted) |
| Auto-update     | pyshipupdate            | Store manages updates          |
| Review required | No                      | Yes (Store only; ~1–3 days)    |
| Revenue share   | None                    | 15% (>$1M/yr: 12%)             |
| Startup support | Yes                     | Requires manifest extension    |

## Testing

Run tests with:

```batch
venv\Scripts\python.exe -m pytest test_pyship/ -v
```

### Environment Variables

| Variable                          | Description                                                                                                                                                                                       | Default                                    |
|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------|
| `PYSHIP_SIGNING_CERTIFICATE_PIN`  | PFX password or hardware token PIN for code signing. Read automatically by `ship()` when `certificate_password` is not set.                                                                       | (not set)                                  |
| `AWSIMPLE_USE_MOTO_MOCK`          | Set to `0` to use real AWS instead of [moto](https://github.com/getmoto/moto) mock. Required for `test_f_update` which tests cross-process S3 updates. Requires valid AWS credentials configured. | `1` (use moto)                             |
| `MAKE_NSIS_PATH`                  | Path to the NSIS `makensis.exe` executable.                                                                                                                                                       | `C:\Program Files (x86)\NSIS\makensis.exe` |

### Test Modes

- **Default (moto mock)**: All tests run with mocked AWS S3. No credentials needed. `test_f_update` is skipped.
- **Real AWS** (`AWSIMPLE_USE_MOTO_MOCK=0`): All tests run against real AWS S3. `test_f_update` runs and tests
  cross-process updates.
