source: trunk/makeMacApp.py @ 4756

Last change on this file since 4756 was 4756, checked in by toby, 9 months ago

more mac work

  • Property svn:eol-style set to native
  • Property svn:keywords set to Date Author Revision URL Id
File size: 11.2 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        # create a link named GSAS-II.py to the script
106        newScript = os.path.join(path2GSAS,'GSAS-II.py')
107        if os.path.exists(newScript): # cleanup
108            print("\nRemoving sym link",newScript)
109            os.remove(newScript)
110        os.symlink(os.path.split(script)[1],newScript)
111        print("\nCreated "+projectname+" app ("+str(appPath)+
112          ").\nViewing app in Finder so you can drag it to the dock if, you wish.")
113        subprocess.call(["open","-R",appPath])
114        sys.exit()
115    else:
116        print('found g2app.tar.gz, but python not in expected location')       
117
118if __name__ == '__main__' and sys.platform == "darwin":
119    iconfile = os.path.join(path2GSAS,'gsas2.icns') # optional icon file
120   
121    AppleScript = '''(*   GSAS-II AppleScript by B. Toby (brian.toby@anl.gov)
122     It can launch GSAS-II by double clicking or by dropping a data file
123     or folder over the app.
124     It runs GSAS-II in a terminal window.
125*)
126
127(* test if a file is present and exit with an error message if it is not  *)
128on TestFilePresent(appwithpath)
129        tell application "System Events"
130                if (file appwithpath exists) then
131                else
132                        display dialog "Error: file " & appwithpath & " not found. If you have moved this file recreate the AppleScript with bootstrap.py." with icon caution buttons {{"Quit"}}
133                        return
134                end if
135        end tell
136end TestFilePresent
137
138(*
139------------------------------------------------------------------------
140this section responds to a double-click. No file is supplied to GSAS-II
141------------------------------------------------------------------------
142*)
143on run
144        set python to "{:s}"
145        set appwithpath to "{:s}"
146        set env to "{:s}"
147        TestFilePresent(appwithpath)
148        TestFilePresent(python)
149        tell application "Terminal"
150                do script env & python & " " & appwithpath & "; exit"
151        end tell
152end run
153
154(*
155-----------------------------------------------------------------------------------------------
156this section handles starting with files dragged into the AppleScript
157 o it goes through the list of file(s) dragged in
158 o then it converts the colon-delimited macintosh file location to a POSIX filename
159 o for every non-directory file dragged into the icon, it starts GSAS-II, passing the file name
160------------------------------------------------------------------------------------------------
161*)
162
163on open names
164        set python to "{:s}"
165        set appwithpath to "{:s}"
166        set env to "{:s}"
167 
168        TestFilePresent(appwithpath)
169        repeat with filename in names
170                set filestr to (filename as string)
171                if filestr ends with ":" then
172                        (* should not happen, skip directories *)
173                else
174                        (* if this is an input file, open it *)
175                        set filename to the quoted form of the POSIX path of filename
176                        tell application "Terminal"
177                                activate
178                                do script env & python & " " & appwithpath & " " & filename & "; exit"
179                        end tell
180                end if
181        end repeat
182end open
183'''
184    # create a link named GSAS-II.py to the script
185    newScript = os.path.join(path2GSAS,'GSAS-II.py')
186    if os.path.exists(newScript): # cleanup
187        print("\nRemoving sym link",newScript)
188        os.remove(newScript)
189    os.symlink(os.path.split(script)[1],newScript)
190    script=newScript
191
192    # find Python used to run GSAS-II and set a new to use to call it
193    # inside the app that will be created
194    pythonExe = os.path.realpath(sys.executable)
195    newpython =  os.path.join(appPath,"Contents","MacOS",projectname)
196   
197    # create an app using this python and if that fails to run wx, look for a
198    # pythonw and try that with wx
199    for i in 1,2,3:
200        if os.path.exists(appPath): # cleanup
201            print("\nRemoving old "+projectname+" app ("+str(appPath)+")")
202            shutil.rmtree(appPath)
203       
204        shell = os.path.join("/tmp/","appscrpt.script")
205        f = open(shell, "w")
206        f.write(AppleScript.format(newpython,script,'',newpython,script,''))
207        f.close()
208
209        try: 
210            subprocess.check_output(["osacompile","-o",appPath,shell],stderr=subprocess.STDOUT)
211        except subprocess.CalledProcessError as msg:
212            print('''Error compiling AppleScript.
213            Report the next message along with details about your Mac to toby@anl.gov''')
214            print(msg.output)
215            sys.exit()
216        # create a link to the python inside the app, if named to match the project
217        if pythonExe != newpython: os.symlink(pythonExe,newpython)
218
219        # test if newpython can run wx
220        testout,errout = RunPython(newpython,'import numpy; import wx; wx.App(); print("-"+"OK-")')
221        if isinstance(testout,bytes): testout = testout.decode()
222        if "-OK-" in testout:
223            print('wxpython app ran',testout)
224            break
225        elif i == 1:
226            print('Run of wx in',pythonExe,'failed, looking for pythonw')
227            pythonExe = os.path.join(os.path.split(sys.executable)[0],'pythonw')
228            if not os.path.exists(pythonExe):
229                print('Warning no pythonw found with ',sys.executable,
230                      '\ncontinuing, hoping for the best')
231        elif i == 2:
232            print('Warning could not run wx with',pythonExe,
233                      'will try with that external to app')
234            newpython = pythonExe
235        else:
236            print('Warning still could not run wx with',pythonExe,
237                      '\ncontinuing, hoping for the best')
238
239    # change the icon
240    oldicon = os.path.join(appPath,"Contents","Resources","droplet.icns")
241    #if os.path.exists(iconfile) and os.path.exists(oldicon):
242    if os.path.exists(iconfile):
243        shutil.copyfile(iconfile,oldicon)
244
245    # Edit the app plist file to restrict the type of files that can be dropped
246    if hasattr(plistlib,'load'):
247        fp = open(os.path.join(appPath,"Contents",'Info.plist'),'rb')
248        d = plistlib.load(fp)
249        fp.close()
250    else:
251        d = plistlib.readPlist(os.path.join(appPath,"Contents",'Info.plist'))
252    d['CFBundleDocumentTypes'] = [{
253        'CFBundleTypeExtensions': ['gpx'],
254        'CFBundleTypeName': 'GSAS-II project',
255        'CFBundleTypeRole': 'Editor'}]
256   
257    if hasattr(plistlib,'dump'):
258        fp = open(os.path.join(appPath,"Contents",'Info.plist'),'wb')
259        plistlib.dump(d,fp)
260        fp.close()
261    else:
262        plistlib.writePlist(d,os.path.join(appPath,"Contents",'Info.plist'))
263
264    # Big Sur: open & save the file in the editor to set authorization levels
265    osascript = '''
266    tell application "Script Editor"
267       set MyName to open "{}"
268       save MyName
269       (* close MyName *)
270       (* quit *)
271    end tell
272'''.format(appPath)
273    # detect MacOS 11 (11.0 == 10.16!)
274    if platform.mac_ver()[0].split('.')[0] == '11' or platform.mac_ver()[0][:5] == '10.16':
275        print("\nFor Big Sur and later, save the app in Script Editor before using it\n")
276        subprocess.Popen(["osascript","-e",osascript])
277    print("\nCreated "+projectname+" app ("+str(appPath)+
278          ").\nViewing app in Finder so you can drag it to the dock if, you wish.")
279    subprocess.call(["open","-R",appPath])
Note: See TracBrowser for help on using the repository browser.