Skip to content

Commit

Permalink
Working on pyapp for binary build
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed Nov 30, 2024
1 parent b3db49f commit 3e9b6df
Show file tree
Hide file tree
Showing 12 changed files with 489 additions and 2 deletions.
4 changes: 4 additions & 0 deletions .bumpversion.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ serialize = {major}.{minor}.{patch}
[bumpversion:file:osxphotos/_version.py]
parse = __version__\s=\s\"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\"
serialize = {major}.{minor}.{patch}

[bumpversion:file:applecrate.toml]
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
serialize = {major}.{minor}.{patch}
16 changes: 16 additions & 0 deletions applecrate.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# TOML file for generating the applecrate package with applecrate
# The version in this file will be updated by bump2version

app = "osxphotos"
version = "0.69.0"
identifier = "org.rhettbull.osxphotos"
license = "LICENSE"
install = [
[
"build/osxphotos-0.69.0-{{ machine }}",
"/usr/local/bin/osxphotos",
],
]
pre_install = "scripts/preinstall.sh"
output = "dist/{{ app }}-{{ version }}-{{ machine }}-installer.pkg"
sign = "$DEVELOPER_ID_INSTALLER"
3 changes: 2 additions & 1 deletion osxphotos/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import click

from osxphotos._constants import PROFILE_SORT_KEYS
from osxphotos._version import __version__
from osxphotos.disclaim import disclaim, pyapp, pyinstaller
from osxphotos.platform import is_macos

Expand Down Expand Up @@ -39,6 +38,7 @@
from .template_repl import template_repl
from .theme import theme
from .tutorial import tutorial
from .update_command import update_command
from .version import version

if is_macos:
Expand Down Expand Up @@ -162,6 +162,7 @@ def at_exit():
template_repl,
uninstall,
version,
update_command,
]

if is_macos:
Expand Down
2 changes: 1 addition & 1 deletion osxphotos/cli/install_uninstall_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def get_usage(self, ctx):
@click.argument("packages", nargs=-1, required=False)
def install(packages, upgrade, requirements_file):
"""Install Python packages into the same environment as osxphotos"""
args = ["pip", "install"]
args = ["pip", "--disable-pip-version-check", "--verbose", "install"]
if upgrade:
args += ["--upgrade"]
if requirements_file:
Expand Down
101 changes: 101 additions & 0 deletions osxphotos/cli/selfupdate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
""" Auto-update the installed version """

from __future__ import annotations

import json
import os
import runpy
import ssl
import subprocess
import sys
import urllib.request
from typing import Iterable

from packaging.version import parse as version_parse

VERSION_INFO_URL = "https://pypi.org/pypi/{}/json"


def pyapp() -> bool:
"""Check if we are running in a pyapp environment."""
return os.environ.get("PYAPP") == "1"


def install(packages: Iterable[str], upgrade: bool = False) -> int:
"""Install Python packages into the same environment as the current script using the pip module.
Args:
packages: The names of the packages to install.
upgrade: Whether to upgrade the packages if they are already installed.
Returns: The exit code of the pip command.
"""
args = ["pip", "--disable-pip-version-check", "--verbose", "install"]
if upgrade:
args += ["--upgrade"]
args += list(packages)
sys.argv = args

# monkey patch sys.exit to catch the exit code when running pip
# otherwise, pip.__main__ will call sys.exit and stop execution
original_exit = sys.exit
exit_code = None

def _exit(code):
nonlocal exit_code
exit_code = code

sys.exit = _exit
try:
runpy.run_module("pip", run_name="__main__")
finally:
sys.exit = original_exit
return exit_code


def update(package: str, version: str, pyapp_binary: str) -> None:
"""Update the installation to the latest version.
Args:
package: The name of the package to update.
version: The current version of the package.
pyapp_binary: The name of the package binary built with PyApp to use for updating.
Note:
Updating PyApp package requires the pyapp_binary to be available in the PATH.
This will work for most users but may fail if the binary is not in the path and
was instead invoked using an absolute path, e.g. /path/to/pyapp_binary.
I am not aware of a way to get the absolute path of the binary from the package
itself as the PyApp binary will execute python, replacing the current process.
"""
if pyapp():
# let pyapp handle the update
command = [pyapp_binary, "self", "update"]
subprocess.run(command, check=True)
return
else:
# otherwise let's update in place
# check if there is a newer version of the package
latest_version = get_latest_version(package)
if version_parse(latest_version) > version_parse(version):
print(f"Updating from version {version} to {latest_version}.")
install([package], upgrade=True)
print(f"Updated {package} to version {latest_version}.")
else:
print(f"{package} is already up to date: {version}.")


def get_latest_version(package_name: str) -> str:
"""Get latest version of package_name from PyPI
Note: This uses the standard library instead of `requests`
to avoid adding a dependency to the project.
"""
try:
url = VERSION_INFO_URL.format(package_name)
ssl_context = ssl._create_unverified_context()
response = urllib.request.urlopen(url, context=ssl_context)
data = json.load(response)
return data["info"]["version"]
except Exception as e:
raise ValueError(f"Error retrieving version for {package_name}: {e}") from e
14 changes: 14 additions & 0 deletions osxphotos/cli/update_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Self-update command for osxphotos"""

import click

from osxphotos._constants import APP_NAME
from osxphotos._version import __version__

from .selfupdate import update


@click.command(name="update")
def update_command():
"""Update the installation to the latest version."""
update(APP_NAME, __version__, APP_NAME)
34 changes: 34 additions & 0 deletions scripts/build_cli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash

# This script is very specific to my particular setup on my machine.
# It must be run after the package has been updated on PyPI.
# It uses `pyapp-runner.sh`, a simple CI script that runs via ssh,
# to build and sign the binaries for the package and then build the installer package.
#
# To run the script, run it from the project root directory:
# ./scripts/build_cli.sh
#

# Get the current version of the package from the source
PACKAGE_NAME="osxphotos"
VERSION=$(grep __version__ $PACKAGE_NAME/_version.py | cut -d "\"" -f 2)

# verify VERSION is valid
# PyApp will happily build with an invalid version number
# get directory of this script
# DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PYPI_VERSION=$(python scripts/get_latest_pypi_version.py $PACKAGE_NAME)
if [ "$PYPI_VERSION" != "$VERSION" ]; then
echo "Invalid version number: $VERSION"
echo "Latest version on PyPI: $PYPI_VERSION"
echo "Did you forget to run 'flit publish'?"
exit 1
fi

# Build the binaries and package them
# arm64 binary built on a remote M1 Mac
# echo "Building version $VERSION for Apple Silicon"
# bash scripts/pyapp-runner.sh m1 $PACKAGE_NAME $VERSION

echo "Building version $VERSION for Intel"
bash scripts/pyapp-runner.sh macbook $PACKAGE_NAME $VERSION
37 changes: 37 additions & 0 deletions scripts/get_latest_pypi_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Given a PyPI package name, print the latest version number of the package.
This uses the standard library instead of requests to avoid adding a dependency to the project.
"""

from __future__ import annotations

import json
import ssl
import sys
import urllib.request

VERSION_INFO_URL = "https://pypi.org/pypi/{}/json"


def get_latest_version(package_name: str) -> str:
"""Get latest version of package_name from PyPI"""
try:
url = VERSION_INFO_URL.format(package_name)
ssl_context = ssl._create_unverified_context()
response = urllib.request.urlopen(url, context=ssl_context)
data = json.load(response)
return data["info"]["version"]
except Exception as e:
raise ValueError(f"Error retrieving version for {package_name}: {e}")


if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} PACKAGE_NAME")
sys.exit(1)
package_name = sys.argv[1]
try:
print(get_latest_version(package_name))
except ValueError as e:
print(e)
sys.exit(1)
17 changes: 17 additions & 0 deletions scripts/postinstall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash

# This script used by applecrate to install the applecrate executable
# into the /usr/local/bin directory
# It will link the correct executable depending on machine CPU
# The applecrate binaries should be named as follows:
# {{ app }}-{{ version }}-x86_64 or {{ app }}-{{ version }}-arm64
# and they should be installed with the following directory structure:
# /Library/Application Support/{{ app }}/{{ version }}/{{ app }}-{{ version }}-x86_64
# /Library/Application Support/{{ app }}/{{ version }}/{{ app }}-{{ version }}-arm64


# CPU will be x86_64 or arm64
CPU=$(uname -m)

ln -s "/Library/Application Support/{{ app }}/{{ version }}/{{ app }}-{{ version }}-$CPU" "/usr/local/bin/applecrate"
chmod 755 "/usr/local/bin/applecrate"
6 changes: 6 additions & 0 deletions scripts/preinstall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash

# Preinstall script for applecrate installer
# This will remove the applecrate binary from /usr/local/bin if it exists

rm -f /usr/local/bin/applecrate
Loading

0 comments on commit 3e9b6df

Please sign in to comment.