Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

building: allow clickable .app on OSX #5419

Draft
wants to merge 17 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion PyInstaller/building/makespec.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,14 @@ def __add_options(parser):
'name for code signing purposes. The usual form is a hierarchical name '
'in reverse DNS notation. For example: com.mycompany.department.appname '
"(default: first script's basename)")
g.add_argument('--osx-app-console', dest='osx_app_console',
action="store_true",
help='The macOS .app bundles don\'t '
'seem to open terminal window by themselves when you '
'run them, e.g. by double clicking on them in Finder. '
'This option adds a launcher script that explicitly '
'opens a MacOS terminal and runs an executable in it. '
'Use in conjunction with `--windowed` option')

g = parser.add_argument_group('Rarely used special options')
g.add_argument("--runtime-tmpdir", dest="runtime_tmpdir", metavar="PATH",
Expand All @@ -448,7 +456,8 @@ def main(scripts, name=None, onefile=None,
console=True, debug=None, strip=False, noupx=False, upx_exclude=None,
runtime_tmpdir=None, pathex=None, version_file=None, specpath=None,
bootloader_ignore_signals=False,
datas=None, binaries=None, icon_file=None, manifest=None, resources=None, bundle_identifier=None,
datas=None, binaries=None, icon_file=None, manifest=None,
resources=None, bundle_identifier=None, osx_app_console=False,
hiddenimports=None, hookspath=None, key=None, runtime_hooks=None,
excludes=None, uac_admin=False, uac_uiaccess=False,
win_no_prefer_redirects=False, win_private_assemblies=False,
Expand Down Expand Up @@ -501,6 +510,8 @@ def main(scripts, name=None, onefile=None,
# We need to encapsulate it into apostrofes.
bundle_identifier = "'%s'" % bundle_identifier

osx_app_console = bool(osx_app_console)

motatoes marked this conversation as resolved.
Show resolved Hide resolved
if manifest:
if "<" in manifest:
# Assume XML string
Expand Down Expand Up @@ -581,6 +592,8 @@ def main(scripts, name=None, onefile=None,
'icon': icon_file,
# .app bundle identifier. Only OSX uses this item.
'bundle_identifier': bundle_identifier,
# Allows .app CLI packages to work on OSX
'osx_app_console': osx_app_console,
# Windows assembly searching options
'win_no_prefer_redirects': win_no_prefer_redirects,
'win_private_assemblies': win_private_assemblies,
Expand Down
24 changes: 24 additions & 0 deletions PyInstaller/building/osx.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

import os
import plistlib
from pathlib import Path
import shutil
import shlex
from ..compat import is_darwin
from .api import EXE, COLLECT
from .datastruct import Target, TOC, logger
Expand Down Expand Up @@ -61,6 +63,10 @@ def __init__(self, *args, **kws):
# Fallback to appname.
self.bundle_identifier = self.appname

# If True generates a wrapper script which allows the .app
# to run as a CLI in a terminal
self.osx_app_console = kws.get('osx_app_console', False)

self.info_plist = kws.get('info_plist', None)

for arg in args:
Expand Down Expand Up @@ -146,6 +152,24 @@ def assemble(self):

}

if self.osx_app_console:
# Make app bundle double-clickable
app_path = Path(self.name)

info_plist_dict['CFBundleExecutable'] = 'wrapper'

# write new wrapper script
shell_script = '''#!/bin/bash
dir=$(cd "$( dirname "${0}")" && pwd )
open -a Terminal "${dir}/%s"''' % self.appname
wrapper_script = app_path / 'Contents/MacOS/wrapper'
with open(wrapper_script, 'w') as f:
f.write(shell_script)

# make it executable
wrapper_script.chmod(0o755)


# Set some default values.
# But they still can be overwritten by the user.
if self.console:
Expand Down
6 changes: 4 additions & 2 deletions PyInstaller/building/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,13 @@
bundleexetmplt = """app = BUNDLE(exe,
name='%(name)s.app',
icon=%(icon)s,
bundle_identifier=%(bundle_identifier)s)
bundle_identifier=%(bundle_identifier)s,
osx_app_console=%(osx_app_console)s)
"""

bundletmplt = """app = BUNDLE(coll,
name='%(name)s.app',
icon=%(icon)s,
bundle_identifier=%(bundle_identifier)s)
bundle_identifier=%(bundle_identifier)s,
osx_app_console=%(osx_app_console)s)
"""
6 changes: 4 additions & 2 deletions doc/spec-files.rst
Original file line number Diff line number Diff line change
Expand Up @@ -377,12 +377,14 @@ create the Mac OS X application bundle, or app folder::
app = BUNDLE(exe,
name='myscript.app',
icon=None,
bundle_identifier=None)
bundle_identifier=None,
osx_app_console=False)

The ``icon=`` argument to ``BUNDLE`` will have the path to an icon file
that you specify using the ``--icon=`` option.
The ``bundle_identifier`` will have the value you specify with the
``--osx-bundle-identifier=`` option.
``--osx-bundle-identifier=`` option. The ``osx_app_console`` will have the
value you specify by ``--osx-app-console`` option.

An :file:`Info.plist` file is an important part of a Mac OS X app bundle.
(See the `Apple bundle overview`_ for a discussion of the contents
Expand Down
1 change: 1 addition & 0 deletions news/5419.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a Mac OS argument `--osx-console-app` which allows
7 changes: 7 additions & 0 deletions tests/functional/scripts/osx console option.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import sys

if sys.stdin.isatty():
f = open("itsaconsole.txt", "w")
f.write("true")
f.close()

31 changes: 31 additions & 0 deletions tests/functional/specs/osx_console_option.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(['../scripts/osx console option.py'])
pyz = PYZ(a.pure, a.zipped_data)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='osx console option',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='osx console option')
app = BUNDLE(coll,
name='osx console option.app',
icon=None,
bundle_identifier=None,
osx_app_console=True)
23 changes: 23 additions & 0 deletions tests/functional/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
# ---------------
import locale
import os
import subprocess
import sys

# Third-party imports
# -------------------
import time

import pytest

# Local imports
Expand Down Expand Up @@ -380,6 +383,26 @@ def test_set_icon(pyi_builder, data_dir):
pyi_builder.test_source("print('Hello Python!')", pyi_args=args)


@pytest.mark.darwin
def test_osx_app_console_option(tmpdir, pyi_builder_spec, monkeypatch):
# -*- mode: python ; coding: utf-8 -*-
app_path = os.path.join(tmpdir, 'dist',
'osx console option.app')
is_console_path = os.path.join(tmpdir, 'dist', 'itsaconsole.txt')


pyi_builder_spec.test_spec('osx_console_option.spec')

# First run using 'open' registers custom protocol handler
subprocess.check_call(['open', app_path])
# 'open' starts program in a different process
# so we need to wait for it to finish
time.sleep(5)

assert os.path.exists(is_console_path), 'did not find confirmation that \
.app is running in console mode'


def test_python_home(pyi_builder):
pyi_builder.test_script('pyi_python_home.py')

Expand Down