|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 const USER_LIB_DIR = OS.Constants.Path.macUserLibDir; |
|
6 const LOCAL_APP_DIR = OS.Constants.Path.macLocalApplicationsDir; |
|
7 |
|
8 /** |
|
9 * Constructor for the Mac native app shell |
|
10 * |
|
11 * @param aApp {Object} the app object provided to the install function |
|
12 * @param aManifest {Object} the manifest data provided by the web app |
|
13 * @param aCategories {Array} array of app categories |
|
14 * @param aRegistryDir {String} (optional) path to the registry |
|
15 */ |
|
16 function NativeApp(aApp, aManifest, aCategories, aRegistryDir) { |
|
17 CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir); |
|
18 |
|
19 // The ${ProfileDir} is: sanitized app name + "-" + manifest url hash |
|
20 this.appProfileDir = OS.Path.join(USER_LIB_DIR, "Application Support", |
|
21 this.uniqueName); |
|
22 this.configJson = "webapp.json"; |
|
23 |
|
24 this.contentsDir = "Contents"; |
|
25 this.macOSDir = OS.Path.join(this.contentsDir, "MacOS"); |
|
26 this.resourcesDir = OS.Path.join(this.contentsDir, "Resources"); |
|
27 this.iconFile = OS.Path.join(this.resourcesDir, "appicon.icns"); |
|
28 this.zipFile = OS.Path.join(this.resourcesDir, "application.zip"); |
|
29 } |
|
30 |
|
31 NativeApp.prototype = { |
|
32 __proto__: CommonNativeApp.prototype, |
|
33 /* |
|
34 * The _rootInstallDir property is the path of the directory where we install |
|
35 * apps. In production code, it's "/Applications". In tests, it's |
|
36 * "~/Applications" because on build machines we don't have enough privileges |
|
37 * to write to the global "/Applications" directory. |
|
38 */ |
|
39 _rootInstallDir: LOCAL_APP_DIR, |
|
40 |
|
41 /** |
|
42 * Creates a native installation of the web app in the OS |
|
43 * |
|
44 * @param aManifest {Object} the manifest data provided by the web app |
|
45 * @param aZipPath {String} path to the zip file for packaged apps (undefined |
|
46 * for hosted apps) |
|
47 */ |
|
48 install: Task.async(function*(aManifest, aZipPath) { |
|
49 if (this._dryRun) { |
|
50 return; |
|
51 } |
|
52 |
|
53 // If the application is already installed, this is a reinstallation. |
|
54 if (WebappOSUtils.getInstallPath(this.app)) { |
|
55 return yield this.prepareUpdate(aManifest, aZipPath); |
|
56 } |
|
57 |
|
58 this._setData(aManifest); |
|
59 |
|
60 let localAppDir = getFile(this._rootInstallDir); |
|
61 if (!localAppDir.isWritable()) { |
|
62 throw("Not enough privileges to install apps"); |
|
63 } |
|
64 |
|
65 let destinationName = yield getAvailableFileName([ this._rootInstallDir ], |
|
66 this.appNameAsFilename, |
|
67 ".app"); |
|
68 |
|
69 let installDir = OS.Path.join(this._rootInstallDir, destinationName); |
|
70 |
|
71 let dir = getFile(TMP_DIR, this.appNameAsFilename + ".app"); |
|
72 dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); |
|
73 let tmpDir = dir.path; |
|
74 |
|
75 try { |
|
76 yield this._createDirectoryStructure(tmpDir); |
|
77 this._copyPrebuiltFiles(tmpDir); |
|
78 yield this._createConfigFiles(tmpDir); |
|
79 |
|
80 if (aZipPath) { |
|
81 yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile)); |
|
82 } |
|
83 |
|
84 yield this._getIcon(tmpDir); |
|
85 } catch (ex) { |
|
86 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); |
|
87 throw ex; |
|
88 } |
|
89 |
|
90 this._removeInstallation(true, installDir); |
|
91 |
|
92 try { |
|
93 // Move the temp installation directory to the /Applications directory |
|
94 yield this._applyTempInstallation(tmpDir, installDir); |
|
95 } catch (ex) { |
|
96 this._removeInstallation(false, installDir); |
|
97 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); |
|
98 throw ex; |
|
99 } |
|
100 }), |
|
101 |
|
102 /** |
|
103 * Creates an update in a temporary directory to be applied later. |
|
104 * |
|
105 * @param aManifest {Object} the manifest data provided by the web app |
|
106 * @param aZipPath {String} path to the zip file for packaged apps (undefined |
|
107 * for hosted apps) |
|
108 */ |
|
109 prepareUpdate: Task.async(function*(aManifest, aZipPath) { |
|
110 if (this._dryRun) { |
|
111 return; |
|
112 } |
|
113 |
|
114 this._setData(aManifest); |
|
115 |
|
116 let [ oldUniqueName, installDir ] = WebappOSUtils.getLaunchTarget(this.app); |
|
117 if (!installDir) { |
|
118 throw ERR_NOT_INSTALLED; |
|
119 } |
|
120 |
|
121 if (this.uniqueName != oldUniqueName) { |
|
122 // Bug 919799: If the app is still in the registry, migrate its data to |
|
123 // the new format. |
|
124 throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME; |
|
125 } |
|
126 |
|
127 let updateDir = OS.Path.join(installDir, "update"); |
|
128 yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); |
|
129 yield OS.File.makeDir(updateDir); |
|
130 |
|
131 try { |
|
132 yield this._createDirectoryStructure(updateDir); |
|
133 this._copyPrebuiltFiles(updateDir); |
|
134 yield this._createConfigFiles(updateDir); |
|
135 |
|
136 if (aZipPath) { |
|
137 yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile)); |
|
138 } |
|
139 |
|
140 yield this._getIcon(updateDir); |
|
141 } catch (ex) { |
|
142 yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); |
|
143 throw ex; |
|
144 } |
|
145 }), |
|
146 |
|
147 /** |
|
148 * Applies an update. |
|
149 */ |
|
150 applyUpdate: Task.async(function*() { |
|
151 if (this._dryRun) { |
|
152 return; |
|
153 } |
|
154 |
|
155 let installDir = WebappOSUtils.getInstallPath(this.app); |
|
156 let updateDir = OS.Path.join(installDir, "update"); |
|
157 |
|
158 let backupDir = yield this._backupInstallation(installDir); |
|
159 |
|
160 try { |
|
161 // Move the update directory to the /Applications directory |
|
162 yield this._applyTempInstallation(updateDir, installDir); |
|
163 } catch (ex) { |
|
164 yield this._restoreInstallation(backupDir, installDir); |
|
165 throw ex; |
|
166 } finally { |
|
167 yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); |
|
168 yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); |
|
169 } |
|
170 }), |
|
171 |
|
172 _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) { |
|
173 yield OS.File.move(OS.Path.join(aTmpDir, this.configJson), |
|
174 OS.Path.join(this.appProfileDir, this.configJson)); |
|
175 |
|
176 yield moveDirectory(aTmpDir, aInstallDir); |
|
177 }), |
|
178 |
|
179 _removeInstallation: function(keepProfile, aInstallDir) { |
|
180 let filesToRemove = [ aInstallDir ]; |
|
181 |
|
182 if (!keepProfile) { |
|
183 filesToRemove.push(this.appProfileDir); |
|
184 } |
|
185 |
|
186 removeFiles(filesToRemove); |
|
187 }, |
|
188 |
|
189 _backupInstallation: Task.async(function*(aInstallDir) { |
|
190 let backupDir = OS.Path.join(aInstallDir, "backup"); |
|
191 yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); |
|
192 yield OS.File.makeDir(backupDir); |
|
193 |
|
194 yield moveDirectory(OS.Path.join(aInstallDir, this.contentsDir), |
|
195 backupDir); |
|
196 yield OS.File.move(OS.Path.join(this.appProfileDir, this.configJson), |
|
197 OS.Path.join(backupDir, this.configJson)); |
|
198 |
|
199 return backupDir; |
|
200 }), |
|
201 |
|
202 _restoreInstallation: Task.async(function*(aBackupDir, aInstallDir) { |
|
203 yield OS.File.move(OS.Path.join(aBackupDir, this.configJson), |
|
204 OS.Path.join(this.appProfileDir, this.configJson)); |
|
205 yield moveDirectory(aBackupDir, |
|
206 OS.Path.join(aInstallDir, this.contentsDir)); |
|
207 }), |
|
208 |
|
209 _createDirectoryStructure: Task.async(function*(aDir) { |
|
210 yield OS.File.makeDir(this.appProfileDir, |
|
211 { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); |
|
212 |
|
213 yield OS.File.makeDir(OS.Path.join(aDir, this.contentsDir), |
|
214 { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); |
|
215 |
|
216 yield OS.File.makeDir(OS.Path.join(aDir, this.macOSDir), |
|
217 { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); |
|
218 |
|
219 yield OS.File.makeDir(OS.Path.join(aDir, this.resourcesDir), |
|
220 { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); |
|
221 }), |
|
222 |
|
223 _copyPrebuiltFiles: function(aDir) { |
|
224 let destDir = getFile(aDir, this.macOSDir); |
|
225 let stub = getFile(this.runtimeFolder, "webapprt-stub"); |
|
226 stub.copyTo(destDir, "webapprt"); |
|
227 }, |
|
228 |
|
229 _createConfigFiles: function(aDir) { |
|
230 // ${ProfileDir}/webapp.json |
|
231 yield writeToFile(OS.Path.join(aDir, this.configJson), |
|
232 JSON.stringify(this.webappJson)); |
|
233 |
|
234 // ${InstallDir}/Contents/MacOS/webapp.ini |
|
235 let applicationINI = getFile(aDir, this.macOSDir, "webapp.ini"); |
|
236 |
|
237 let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. |
|
238 getService(Ci.nsIINIParserFactory). |
|
239 createINIParser(applicationINI). |
|
240 QueryInterface(Ci.nsIINIParserWriter); |
|
241 writer.setString("Webapp", "Name", this.appName); |
|
242 writer.setString("Webapp", "Profile", this.uniqueName); |
|
243 writer.writeFile(); |
|
244 applicationINI.permissions = PERMS_FILE; |
|
245 |
|
246 // ${InstallDir}/Contents/Info.plist |
|
247 let infoPListContent = '<?xml version="1.0" encoding="UTF-8"?>\n\ |
|
248 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n\ |
|
249 <plist version="1.0">\n\ |
|
250 <dict>\n\ |
|
251 <key>CFBundleDevelopmentRegion</key>\n\ |
|
252 <string>English</string>\n\ |
|
253 <key>CFBundleDisplayName</key>\n\ |
|
254 <string>' + escapeXML(this.appName) + '</string>\n\ |
|
255 <key>CFBundleExecutable</key>\n\ |
|
256 <string>webapprt</string>\n\ |
|
257 <key>CFBundleIconFile</key>\n\ |
|
258 <string>appicon</string>\n\ |
|
259 <key>CFBundleIdentifier</key>\n\ |
|
260 <string>' + escapeXML(this.uniqueName) + '</string>\n\ |
|
261 <key>CFBundleInfoDictionaryVersion</key>\n\ |
|
262 <string>6.0</string>\n\ |
|
263 <key>CFBundleName</key>\n\ |
|
264 <string>' + escapeXML(this.appName) + '</string>\n\ |
|
265 <key>CFBundlePackageType</key>\n\ |
|
266 <string>APPL</string>\n\ |
|
267 <key>CFBundleVersion</key>\n\ |
|
268 <string>0</string>\n\ |
|
269 <key>NSHighResolutionCapable</key>\n\ |
|
270 <true/>\n\ |
|
271 <key>NSPrincipalClass</key>\n\ |
|
272 <string>GeckoNSApplication</string>\n\ |
|
273 <key>FirefoxBinary</key>\n\ |
|
274 #expand <string>__MOZ_MACBUNDLE_ID__</string>\n\ |
|
275 </dict>\n\ |
|
276 </plist>'; |
|
277 |
|
278 yield writeToFile(OS.Path.join(aDir, this.contentsDir, "Info.plist"), |
|
279 infoPListContent); |
|
280 }, |
|
281 |
|
282 /** |
|
283 * Process the icon from the imageStream as retrieved from |
|
284 * the URL by getIconForApp(). This will bundle the icon to the |
|
285 * app package at Contents/Resources/appicon.icns. |
|
286 * |
|
287 * @param aMimeType the icon mimetype |
|
288 * @param aImageStream the stream for the image data |
|
289 * @param aDir the directory where the icon should be stored |
|
290 */ |
|
291 _processIcon: function(aMimeType, aIcon, aDir) { |
|
292 let deferred = Promise.defer(); |
|
293 |
|
294 function conversionDone(aSubject, aTopic) { |
|
295 if (aTopic == "process-finished") { |
|
296 deferred.resolve(); |
|
297 } else { |
|
298 deferred.reject("Failure converting icon, exit code: " + aSubject.exitValue); |
|
299 } |
|
300 } |
|
301 |
|
302 let process = Cc["@mozilla.org/process/util;1"]. |
|
303 createInstance(Ci.nsIProcess); |
|
304 let sipsFile = getFile("/usr/bin/sips"); |
|
305 |
|
306 process.init(sipsFile); |
|
307 process.runAsync(["-s", "format", "icns", |
|
308 aIcon.path, |
|
309 "--out", OS.Path.join(aDir, this.iconFile), |
|
310 "-z", "128", "128"], |
|
311 9, conversionDone); |
|
312 |
|
313 return deferred.promise; |
|
314 } |
|
315 } |