Skip to content

Commit 9333cf3

Browse files
committed
Call disclaim from cli entry point
1 parent f5adecb commit 9333cf3

File tree

5 files changed

+104
-13
lines changed

5 files changed

+104
-13
lines changed

osxphotos.spec

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ block_cipher = None
5555
a = Analysis(
5656
["cli.py"],
5757
pathex=[pathex],
58-
binaries=[("build/libdisclaim.dylib", ".")],
58+
binaries=[("build/libdisclaim.dylib", "osxphotos")],
5959
datas=datas,
6060
hiddenimports=["pkg_resources.py2_warn"],
6161
hookspath=[],
62-
runtime_hooks=["disclaim.py"],
62+
# runtime_hooks=["disclaim.py"],
6363
excludes=[],
6464
win_no_prefer_redirects=False,
6565
win_private_assemblies=False,

osxphotos/cli/cli.py

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from osxphotos._constants import PROFILE_SORT_KEYS
1111
from osxphotos._version import __version__
12+
from osxphotos.disclaim import disclaim, pyinstaller, pyapp
1213
from osxphotos.platform import is_macos
1314

1415
from .about import about
@@ -106,6 +107,11 @@ def cli_main(ctx, profile, profile_sort, **kwargs):
106107
# the debug options are handled in cli/__init__.py
107108
# before this function is called
108109
ctx.obj = CLI_Obj(group=cli_main)
110+
111+
if pyinstaller() or pyapp():
112+
# Running from executable, run disclaimer
113+
disclaim()
114+
109115
if profile:
110116
click.echo("Profiling...")
111117
profile_sort = profile_sort or ["cumulative"]

osxphotos/cli/export.py

+11-11
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
from osxphotos.path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
6565
from osxphotos.photoexporter import PhotoExporter
6666
from osxphotos.photoinfo import PhotoInfoNone
67+
from osxphotos.photokit_utils import wait_for_photokit_authorization
6768
from osxphotos.photoquery import load_uuid_from_file, query_options_from_kwargs
6869
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
6970
from osxphotos.platform import get_macos_version, is_macos
@@ -1684,17 +1685,16 @@ def export_cli(
16841685
else:
16851686
report_writer = ReportWriterNoOp()
16861687

1687-
# if use_photokit and not check_photokit_authorization():
1688-
# click.echo(
1689-
# "Requesting access to use your Photos library. Click 'OK' on the dialog box to grant access."
1690-
# )
1691-
# request_photokit_authorization()
1692-
# click.confirm("Have you granted access?")
1693-
# if not check_photokit_authorization():
1694-
# click.echo(
1695-
# "Failed to get access to the Photos library which is needed with `--use-photokit`."
1696-
# )
1697-
# return
1688+
if (use_photokit or use_photos_export) and not check_photokit_authorization():
1689+
click.echo(
1690+
"Requesting access to use your Photos library. "
1691+
"Click 'Allow Access to All Photos' in the dialog box to grant access."
1692+
)
1693+
if not wait_for_photokit_authorization():
1694+
rich_click_echo(
1695+
f"[error]Error: could not get authorization to access Photos library.[/]"
1696+
)
1697+
return 1
16981698

16991699
# initialize export flags
17001700
# by default, will export all versions of photos unless skip flag is set

osxphotos/disclaim.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
""" Disclaim the application when running on macOS so that permission requests come from the application itself
2+
instead of the terminal.
3+
4+
To use this, the libdisclaim.dylib library must be built and placed in the same directory as this file
5+
or provided as an argument to the disclaim function.
6+
7+
Reference: http://qt.io/blog/the-curious-case-of-the-responsible-process
8+
"""
9+
10+
import ctypes
11+
import os
12+
import sys
13+
14+
15+
def pyinstaller() -> bool:
16+
"""Return True if the application is running from a PyInstaller bundle."""
17+
# PyInstaller bootloader sets a flag
18+
return hasattr(sys, "_MEIPASS")
19+
20+
21+
def pyapp() -> bool:
22+
"""Check if we are running in a pyapp environment."""
23+
return os.environ.get("PYAPP") == "1"
24+
25+
26+
def disclaim(library_path: str | None = None):
27+
"""Run this function to disclaim the application and set the responsible process to the caller.
28+
29+
Args:
30+
library_path: The path to the libdisclaim.dylib library.
31+
If not provided, libdisclaim.dylib will be loaded from the the same directory as this file.
32+
"""
33+
34+
if sys.platform != "darwin":
35+
return
36+
37+
# Avoid redundant disclaims
38+
env_marker = f"PY_DISCLAIMED-{sys.argv[0]}"
39+
if os.environ.get(env_marker):
40+
return
41+
os.environ[env_marker] = "1"
42+
43+
if pyinstaller():
44+
# If running from pyinstaller, the _MEIPASS2 environment variable is set
45+
# The bootloader has cleared the _MEIPASS2 environment variable by the
46+
# time we get here, which means re-launching the executable disclaimed
47+
# will unpack the binary again. To avoid this we reset _MEIPASS2 again,
48+
# so that our re-launch will pick up at second stage of the bootstrap.
49+
os.environ["_MEIPASS2"] = sys._MEIPASS
50+
51+
# Load the disclaim library and call the disclaim function
52+
library_path = library_path or os.path.join(
53+
os.path.dirname(__file__), "libdisclaim.dylib"
54+
)
55+
libdisclaim = ctypes.cdll.LoadLibrary(library_path)
56+
libdisclaim.disclaim()

osxphotos/photokit_utils.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
""" Utilities for working with the Photokit framework on macOS """
2+
3+
import time
4+
5+
from osxphotos.platform import is_macos
6+
7+
if is_macos:
8+
from osxphotos.photokit import (
9+
check_photokit_authorization,
10+
request_photokit_authorization,
11+
)
12+
13+
# seconds to wait for user to grant authorization
14+
WAIT_FOR_AUTHORIZATION_TIMEOUT = 10
15+
16+
# seconds to sleep between authorization check
17+
AUTHORIZATION_SLEEP = 0.25
18+
19+
def wait_for_photokit_authorization() -> bool:
20+
"""Request and wait for authorization to access Photos library."""
21+
if check_photokit_authorization():
22+
return True
23+
start_time = time.time()
24+
request_photokit_authorization()
25+
while not check_photokit_authorization():
26+
time.sleep(AUTHORIZATION_SLEEP)
27+
if time.time() > start_time + WAIT_FOR_AUTHORIZATION_TIMEOUT:
28+
return False
29+
return True

0 commit comments

Comments
 (0)