Skip to content

Commit

Permalink
Added package signing
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed Feb 4, 2024
1 parent 7bdbb99 commit be5c809
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 6 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Options:
-w, --welcome FILE Path to welcome markdown or HTML file
-c, --conclusion FILE Path to conclusion markdown or HTML file
-u, --uninstall FILE Path to uninstall script; if not provided, an
uninstall script will be created for you.See
uninstall script will be created for you. See
also '--no-uninstall'
-U, --no-uninstall Do not include an uninstall script in the
package
Expand Down Expand Up @@ -84,14 +84,16 @@ Options:
-P, --post-install FILE Path to post-install shell script; if not
provided, a post-install script will be
created for you.
-s, --sign APPLE_DEVELOPER_CERTIFICATE_ID
Sign the installer package with a developer ID
--help Show this message and exit.
```
<!--[[[end]]] -->

## To Do

- [ ] Add support for signing the installer with a developer certificate
- [X] Add support for signing the installer with a developer certificate
- [ ] Add support for notarizing the installer
- [ ] Add python API to create installers programmatically
- [ ] Add `applecrate init` command to create a TOML configuration via a wizard
Expand Down
3 changes: 2 additions & 1 deletion README_DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ Linting and formatting utilizes [ruff](https://github.com/astral-sh/ruff)

## Updating the README

- `cog -r README.md`
- `flit install` to install the latest version of the package
- `cog -r README.md` to update the CLI help in README.md

## Updating version

Expand Down
36 changes: 36 additions & 0 deletions applecrate/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import os
import pathlib
import shutil
import subprocess
Expand Down Expand Up @@ -67,6 +68,10 @@ def check_dependencies():
raise click.ClickException("pkgbuild is not installed")
if not shutil.which("productbuild"):
raise click.ClickException("productbuild is not installed")
if not shutil.which("productsign"):
raise click.ClickException("productsign is not installed")
if not shutil.which("pkgutil"):
raise click.ClickException("pkgutil is not installed")


def build_package(app: str, version: str, target_directory: str):
Expand Down Expand Up @@ -115,6 +120,37 @@ def build_product(app: str, version: str, target_directory: str):
echo(f"Created {product}")


def sign_product(
product_path: str | os.PathLike,
signed_product_path: str | os.PathLike,
certificate_id: str,
):
"""Sign the macOS installer package."""
proc = subprocess.run(
[
"productsign",
"--sign",
f"Developer ID Installer: {certificate_id}",
str(product_path),
str(signed_product_path),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if proc.returncode != 0:
raise click.ClickException(f"productsign failed: {proc.returncode} {proc.stderr.decode('utf-8')}")
echo(f"Signed {product_path} to {signed_product_path}")

proc = subprocess.run(
["pkgutil", "--check-signature", signed_product_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if proc.returncode != 0:
raise click.ClickException(f"pkgutil signature check failed: {proc.returncode} {proc.stderr.decode('utf-8')}")
echo(f"Checked signature of {signed_product_path}")


def stage_install_files(src: str, dest: str, build_dir: pathlib.Path):
"""Stage install files in the build directory."""
src = pathlib.Path(src)
Expand Down
31 changes: 28 additions & 3 deletions applecrate/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
check_dependencies,
clean_build_dir,
create_build_dirs,
sign_product,
stage_install_files,
)
from .template_utils import (
Expand All @@ -29,7 +30,11 @@
render_template,
render_template_from_file,
)
from .utils import copy_and_create_parents, set_from_defaults
from .utils import (
check_certificate_is_valid,
copy_and_create_parents,
set_from_defaults,
)


@click.group()
Expand Down Expand Up @@ -135,6 +140,12 @@ def cli():
type=click.Path(dir_okay=False, exists=True),
help="Path to post-install shell script; " "if not provided, a post-install script will be created for you.",
)
@click.option(
"--sign",
"-s",
metavar="APPLE_DEVELOPER_CERTIFICATE_ID",
help="Sign the installer package with a developer ID",
)
def build(**kwargs):
"""applecrate: A Python package for creating macOS installer packages."""

Expand Down Expand Up @@ -166,6 +177,7 @@ def build(**kwargs):
banner = kwargs["banner"]
post_install = kwargs["post_install"]
pre_install = kwargs["pre_install"]
sign = kwargs["sign"]

# template data
data = {
Expand Down Expand Up @@ -265,10 +277,19 @@ def build(**kwargs):
# Build the macOS installer product
echo("Building the macOS installer product")
build_product(app, version, BUILD_DIR)
product = f"{app}-{version}.pkg"
product_path = BUILD_DIR / "pkg" / product

# sign the installer package
if sign:
signed_product_path = BUILD_DIR / "pkg-signed" / f"{app}-{version}.pkg"
signed_product_path.parent.mkdir(parents=True, exist_ok=True)
echo(f"Signing the installer package with certificate ID: {sign}")
sign_product(product_path, signed_product_path, sign)

echo("Copying installer package to build directory")
product = f"{app}-{version}.pkg"
shutil.copy(BUILD_DIR / "pkg" / product, BUILD_ROOT / product)
product_path = product_path if not sign else signed_product_path
shutil.copy(product_path, BUILD_ROOT / product)

echo(f"Created {BUILD_ROOT / product}")
echo("Done!")
Expand Down Expand Up @@ -364,6 +385,10 @@ def validate_build_kwargs(**kwargs):
raise ValueError("Banner image must be a PNG file")
kwargs["banner"] = banner

if sign := kwargs.get("sign"):
if not check_certificate_is_valid(sign):
raise ValueError(f"Invalid certificate ID: {sign}")


def load_from_toml(path: str | os.PathLike) -> dict[str, str]:
"""Load the [tool.applecrate] from a TOML file."""
Expand Down
14 changes: 14 additions & 0 deletions applecrate/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import copy
import pathlib
import shutil
import subprocess
from typing import Any

from click import echo
Expand All @@ -31,3 +32,16 @@ def copy_and_create_parents(src: pathlib.Path, dst: pathlib.Path):
echo(f"Copying {src} to {dst}")
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(src, dst)


def check_certificate_is_valid(certificate: str) -> bool:
"""Check if a certificate is valid.
Args:
certificate: The certificate to check.
Returns: True if the certificate is valid, False otherwise.
"""

status = subprocess.run(["security", "find-identity", "-v"], capture_output=True)
return certificate in status.stdout.decode("utf-8")

0 comments on commit be5c809

Please sign in to comment.