1 | #!/usr/bin/env python |
---|
2 | ''' |
---|
3 | *makeMacApp: Create Mac Applet* |
---|
4 | =============================== |
---|
5 | |
---|
6 | This script creates an AppleScript app bundle to launch GSAS-II. The app is |
---|
7 | usually created in the directory where the GSAS-II script (.../GSASII/GSASII.py) |
---|
8 | is located. A softlink to Python is created inside that app bundle, |
---|
9 | but the softlink name is GSAS-II so that "GSAS-II" shows up as the name |
---|
10 | of the app in the menu bar, etc. rather than "Python". A soft link named |
---|
11 | GSAS-II.py, referencing the GSASII.py script, is created so that some file |
---|
12 | menu items also are labeled with GSAS-II (but not the right capitalization, |
---|
13 | alas). |
---|
14 | |
---|
15 | This 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 | |
---|
28 | This has been tested with several versions of Python interpreters |
---|
29 | from Anaconda and does not require pythonw (Python.app). |
---|
30 | |
---|
31 | Run this script with no arguments or with one or two arguments. |
---|
32 | |
---|
33 | The first argument, if supplied, is a reference to the GSASII.py script, |
---|
34 | which can have a relative or absolute path (the absolute path is determined). |
---|
35 | If not supplied, the GSASII.py script will be used from the directory where |
---|
36 | this (makeMacApp.py) script is found. |
---|
37 | |
---|
38 | The second argument, if supplied, |
---|
39 | provides the name/location for the app to be created. This can be used to create |
---|
40 | multiple app copies using different Python versions (likely use for |
---|
41 | development only). If the second argument is used, the AppleScript is created rather than |
---|
42 | restored from g2app.tar.gz |
---|
43 | ''' |
---|
44 | |
---|
45 | from __future__ import division, print_function |
---|
46 | import sys, os, os.path, stat, shutil, subprocess, plistlib |
---|
47 | import platform |
---|
48 | def Usage(): |
---|
49 | print("\n\tUsage: python "+sys.argv[0]+" [<GSAS-II script>] [project]\n") |
---|
50 | sys.exit() |
---|
51 | |
---|
52 | def 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 | |
---|
64 | AppleScript = '' |
---|
65 | '''Contains an AppleScript to start GSAS-II, launching Python and the |
---|
66 | GSAS-II python script. |
---|
67 | ''' |
---|
68 | |
---|
69 | if __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 |
---|
98 | if __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 | |
---|
118 | if __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 *) |
---|
128 | on 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 |
---|
136 | end TestFilePresent |
---|
137 | |
---|
138 | (* |
---|
139 | ------------------------------------------------------------------------ |
---|
140 | this section responds to a double-click. No file is supplied to GSAS-II |
---|
141 | ------------------------------------------------------------------------ |
---|
142 | *) |
---|
143 | on 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 |
---|
152 | end run |
---|
153 | |
---|
154 | (* |
---|
155 | ----------------------------------------------------------------------------------------------- |
---|
156 | this 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 | |
---|
163 | on 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 |
---|
182 | end 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]) |
---|