source: trunk/makeMacApp.py @ 4754

Last change on this file since 4754 was 4754, checked in by toby, 10 months ago

new approach for MacOS AppleScripting?

  • Property svn:eol-style set to native
  • Property svn:keywords set to Date Author Revision URL Id
File size: 10.9 KB
Line 
1#!/usr/bin/env python
2'''
3*makeMacApp: Create Mac Applet*
4===============================
5
6This script creates an AppleScript app bundle to launch GSAS-II. The app is
7usually created in the directory where the GSAS-II script (.../GSASII/GSASII.py)
8is located. A softlink to Python is created inside that app bundle,
9but the softlink name is GSAS-II so that "GSAS-II" shows up as the name
10of the app in the menu bar, etc. rather than "Python". A soft link named
11GSAS-II.py, referencing the GSASII.py script, is created so that some file
12menu items also are labeled with GSAS-II (but not the right capitalization,
13alas).
14
15This can be used two different ways.
16
17 1. In the usual way, for conda-type installations
18    where Python is in <condaroot>/bin and GSAS-II is in <condaroot>/GSASII, a premade
19    app is restored from a tar file. This works best for 11.0 (Big Sur) where there are security
20    constraints in place.
21
22 2. If python is not in that location or a name/location is specified
23    for the app that will be created, this script creates an app (AppleScript) with the GSAS-II
24    and the python locations hard coded. When an AppleScript is created, 
25    this script tests to make sure that a wxpython script will run inside the
26    app and if not, it searches for a pythonw image and tries that.
27
28This has been tested with several versions of Python interpreters
29from Anaconda and does not require pythonw (Python.app).
30
31Run this script with no arguments or with one or two arguments.
32
33The first argument, if supplied, is a reference to the GSASII.py script,
34which can have a relative or absolute path (the absolute path is determined).
35If not supplied, the GSASII.py script will be used from the directory where
36this (makeMacApp.py) script is found.
37
38The second argument, if supplied,
39provides the name/location for the app to be created. This can be used to create
40multiple app copies using different Python versions (likely use for
41development only). If the second argument is used, the AppleScript is created rather than
42restored from g2app.tar.gz
43'''
44
45from __future__ import division, print_function
46import sys, os, os.path, stat, shutil, subprocess, plistlib
47import platform
48def Usage():
49    print("\n\tUsage: python "+sys.argv[0]+" [<GSAS-II script>] [project]\n")
50    sys.exit()
51
52def RunPython(image,cmd):
53    'Run a command in a python image'
54    try:
55        err=None
56        p = subprocess.Popen([image,'-c',cmd],stdout=subprocess.PIPE,stderr=subprocess.PIPE)
57        out = p.stdout.read()
58        err = p.stderr.read()
59        p.communicate()
60        return out,err
61    except Exception(err):
62        return '','Exception = '+err
63
64AppleScript = ''
65'''Contains an AppleScript to start GSAS-II, launching Python and the
66GSAS-II python script.
67'''
68
69if __name__ == '__main__':
70    project="GSAS-II"
71    # set GSAS-II location (path2GSAS): find the main GSAS-II script
72    # from this file, if not on command line
73    if len(sys.argv) == 1:
74        path2GSAS = os.path.split(__file__)[0]
75        script = os.path.abspath(os.path.join(path2GSAS,"GSASII.py"))
76    elif len(sys.argv) == 2:
77        script = os.path.abspath(sys.argv[1])
78        path2GSAS = os.path.split(script)[0]
79    elif len(sys.argv) == 3:
80        script = os.path.abspath(sys.argv[1])
81        path2GSAS = os.path.split(script)[0]
82        project = sys.argv[2]
83    else:
84        Usage()
85    if not os.path.exists(script):
86        print("\nFile "+script+" not found")
87        Usage()
88    if os.path.splitext(script)[1].lower() != '.py':
89        print("\nScript "+script+" does not have extension .py")
90        Usage()
91    projectname = os.path.split(project)[1]
92    if os.path.split(project)[0] != '':
93        appPath = os.path.abspath(project+".app")
94    else:
95        appPath = os.path.abspath(os.path.join(path2GSAS,project+".app"))
96
97# new approach, if possible use previously created file
98if __name__ == '__main__' and sys.platform == "darwin" and os.path.exists(
99                os.path.join(path2GSAS,"g2app.tar.gz")) and project =="GSAS-II":
100    if os.path.exists(os.path.join(path2GSAS,'../bin/python')):
101        print('found python, found g2app.tar.gz')
102        subprocess.call(["rm","-rf",appPath])
103        subprocess.call(["mkdir","-p",appPath])
104        subprocess.call(["tar","xzvf",os.path.join(path2GSAS,"g2app.tar.gz"),'-C',appPath])
105        print("\nCreated "+projectname+" app ("+str(appPath)+
106          ").\nViewing app in Finder so you can drag it to the dock if, you wish.")
107        subprocess.call(["open","-R",appPath])
108        sys.exit()
109    else:
110        print('found g2app.tar.gz, but python not in expected location')       
111
112if __name__ == '__main__' and sys.platform == "darwin":
113    iconfile = os.path.join(path2GSAS,'gsas2.icns') # optional icon file
114   
115    AppleScript = '''(*   GSAS-II AppleScript by B. Toby (brian.toby@anl.gov)
116     It can launch GSAS-II by double clicking or by dropping a data file
117     or folder over the app.
118     It runs GSAS-II in a terminal window.
119*)
120
121(* test if a file is present and exit with an error message if it is not  *)
122on TestFilePresent(appwithpath)
123        tell application "System Events"
124                if (file appwithpath exists) then
125                else
126                        display dialog "Error: file " & appwithpath & " not found. If you have moved this file recreate the AppleScript with bootstrap.py." with icon caution buttons {{"Quit"}}
127                        return
128                end if
129        end tell
130end TestFilePresent
131
132(*
133------------------------------------------------------------------------
134this section responds to a double-click. No file is supplied to GSAS-II
135------------------------------------------------------------------------
136*)
137on run
138        set python to "{:s}"
139        set appwithpath to "{:s}"
140        set env to "{:s}"
141        TestFilePresent(appwithpath)
142        TestFilePresent(python)
143        tell application "Terminal"
144                do script env & python & " " & appwithpath & "; exit"
145        end tell
146end run
147
148(*
149-----------------------------------------------------------------------------------------------
150this section handles starting with files dragged into the AppleScript
151 o it goes through the list of file(s) dragged in
152 o then it converts the colon-delimited macintosh file location to a POSIX filename
153 o for every non-directory file dragged into the icon, it starts GSAS-II, passing the file name
154------------------------------------------------------------------------------------------------
155*)
156
157on open names
158        set python to "{:s}"
159        set appwithpath to "{:s}"
160        set env to "{:s}"
161 
162        TestFilePresent(appwithpath)
163        repeat with filename in names
164                set filestr to (filename as string)
165                if filestr ends with ":" then
166                        (* should not happen, skip directories *)
167                else
168                        (* if this is an input file, open it *)
169                        set filename to the quoted form of the POSIX path of filename
170                        tell application "Terminal"
171                                activate
172                                do script env & python & " " & appwithpath & " " & filename & "; exit"
173                        end tell
174                end if
175        end repeat
176end open
177'''
178    # create a link named GSAS-II.py to the script
179    newScript = os.path.join(path2GSAS,'GSAS-II.py')
180    if os.path.exists(newScript): # cleanup
181        print("\nRemoving sym link",newScript)
182        os.remove(newScript)
183    os.symlink(os.path.split(script)[1],newScript)
184    script=newScript
185
186    # find Python used to run GSAS-II and set a new to use to call it
187    # inside the app that will be created
188    pythonExe = os.path.realpath(sys.executable)
189    newpython =  os.path.join(appPath,"Contents","MacOS",projectname)
190   
191    # create an app using this python and if that fails to run wx, look for a
192    # pythonw and try that with wx
193    for i in 1,2,3:
194        if os.path.exists(appPath): # cleanup
195            print("\nRemoving old "+projectname+" app ("+str(appPath)+")")
196            shutil.rmtree(appPath)
197       
198        shell = os.path.join("/tmp/","appscrpt.script")
199        f = open(shell, "w")
200        f.write(AppleScript.format(newpython,script,'',newpython,script,''))
201        f.close()
202
203        try: 
204            subprocess.check_output(["osacompile","-o",appPath,shell],stderr=subprocess.STDOUT)
205        except subprocess.CalledProcessError as msg:
206            print('''Error compiling AppleScript.
207            Report the next message along with details about your Mac to toby@anl.gov''')
208            print(msg.output)
209            sys.exit()
210        # create a link to the python inside the app, if named to match the project
211        if pythonExe != newpython: os.symlink(pythonExe,newpython)
212
213        # test if newpython can run wx
214        testout,errout = RunPython(newpython,'import numpy; import wx; wx.App(); print("-"+"OK-")')
215        if isinstance(testout,bytes): testout = testout.decode()
216        if "-OK-" in testout:
217            print('wxpython app ran',testout)
218            break
219        elif i == 1:
220            print('Run of wx in',pythonExe,'failed, looking for pythonw')
221            pythonExe = os.path.join(os.path.split(sys.executable)[0],'pythonw')
222            if not os.path.exists(pythonExe):
223                print('Warning no pythonw found with ',sys.executable,
224                      '\ncontinuing, hoping for the best')
225        elif i == 2:
226            print('Warning could not run wx with',pythonExe,
227                      'will try with that external to app')
228            newpython = pythonExe
229        else:
230            print('Warning still could not run wx with',pythonExe,
231                      '\ncontinuing, hoping for the best')
232
233    # change the icon
234    oldicon = os.path.join(appPath,"Contents","Resources","droplet.icns")
235    #if os.path.exists(iconfile) and os.path.exists(oldicon):
236    if os.path.exists(iconfile):
237        shutil.copyfile(iconfile,oldicon)
238
239    # Edit the app plist file to restrict the type of files that can be dropped
240    if hasattr(plistlib,'load'):
241        fp = open(os.path.join(appPath,"Contents",'Info.plist'),'rb')
242        d = plistlib.load(fp)
243        fp.close()
244    else:
245        d = plistlib.readPlist(os.path.join(appPath,"Contents",'Info.plist'))
246    d['CFBundleDocumentTypes'] = [{
247        'CFBundleTypeExtensions': ['gpx'],
248        'CFBundleTypeName': 'GSAS-II project',
249        'CFBundleTypeRole': 'Editor'}]
250   
251    if hasattr(plistlib,'dump'):
252        fp = open(os.path.join(appPath,"Contents",'Info.plist'),'wb')
253        plistlib.dump(d,fp)
254        fp.close()
255    else:
256        plistlib.writePlist(d,os.path.join(appPath,"Contents",'Info.plist'))
257
258    # Big Sur: open & save the file in the editor to set authorization levels
259    osascript = '''
260    tell application "Script Editor"
261       set MyName to open "{}"
262       save MyName
263       (* close MyName *)
264       (* quit *)
265    end tell
266'''.format(appPath)
267    # detect MacOS 11 (11.0 == 10.16!)
268    if platform.mac_ver()[0].split('.')[0] == '11' or platform.mac_ver()[0][:5] == '10.16':
269        print("\nFor Big Sur and later, save the app in Script Editor before using it\n")
270        subprocess.Popen(["osascript","-e",osascript])
271    print("\nCreated "+projectname+" app ("+str(appPath)+
272          ").\nViewing app in Finder so you can drag it to the dock if, you wish.")
273    subprocess.call(["open","-R",appPath])
Note: See TracBrowser for help on using the repository browser.