diff -r 000000000000 -r 6474c204b198 toolkit/webapps/MacNativeApp.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/webapps/MacNativeApp.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,315 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const USER_LIB_DIR = OS.Constants.Path.macUserLibDir; +const LOCAL_APP_DIR = OS.Constants.Path.macLocalApplicationsDir; + +/** + * Constructor for the Mac native app shell + * + * @param aApp {Object} the app object provided to the install function + * @param aManifest {Object} the manifest data provided by the web app + * @param aCategories {Array} array of app categories + * @param aRegistryDir {String} (optional) path to the registry + */ +function NativeApp(aApp, aManifest, aCategories, aRegistryDir) { + CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir); + + // The ${ProfileDir} is: sanitized app name + "-" + manifest url hash + this.appProfileDir = OS.Path.join(USER_LIB_DIR, "Application Support", + this.uniqueName); + this.configJson = "webapp.json"; + + this.contentsDir = "Contents"; + this.macOSDir = OS.Path.join(this.contentsDir, "MacOS"); + this.resourcesDir = OS.Path.join(this.contentsDir, "Resources"); + this.iconFile = OS.Path.join(this.resourcesDir, "appicon.icns"); + this.zipFile = OS.Path.join(this.resourcesDir, "application.zip"); +} + +NativeApp.prototype = { + __proto__: CommonNativeApp.prototype, + /* + * The _rootInstallDir property is the path of the directory where we install + * apps. In production code, it's "/Applications". In tests, it's + * "~/Applications" because on build machines we don't have enough privileges + * to write to the global "/Applications" directory. + */ + _rootInstallDir: LOCAL_APP_DIR, + + /** + * Creates a native installation of the web app in the OS + * + * @param aManifest {Object} the manifest data provided by the web app + * @param aZipPath {String} path to the zip file for packaged apps (undefined + * for hosted apps) + */ + install: Task.async(function*(aManifest, aZipPath) { + if (this._dryRun) { + return; + } + + // If the application is already installed, this is a reinstallation. + if (WebappOSUtils.getInstallPath(this.app)) { + return yield this.prepareUpdate(aManifest, aZipPath); + } + + this._setData(aManifest); + + let localAppDir = getFile(this._rootInstallDir); + if (!localAppDir.isWritable()) { + throw("Not enough privileges to install apps"); + } + + let destinationName = yield getAvailableFileName([ this._rootInstallDir ], + this.appNameAsFilename, + ".app"); + + let installDir = OS.Path.join(this._rootInstallDir, destinationName); + + let dir = getFile(TMP_DIR, this.appNameAsFilename + ".app"); + dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + let tmpDir = dir.path; + + try { + yield this._createDirectoryStructure(tmpDir); + this._copyPrebuiltFiles(tmpDir); + yield this._createConfigFiles(tmpDir); + + if (aZipPath) { + yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile)); + } + + yield this._getIcon(tmpDir); + } catch (ex) { + yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); + throw ex; + } + + this._removeInstallation(true, installDir); + + try { + // Move the temp installation directory to the /Applications directory + yield this._applyTempInstallation(tmpDir, installDir); + } catch (ex) { + this._removeInstallation(false, installDir); + yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); + throw ex; + } + }), + + /** + * Creates an update in a temporary directory to be applied later. + * + * @param aManifest {Object} the manifest data provided by the web app + * @param aZipPath {String} path to the zip file for packaged apps (undefined + * for hosted apps) + */ + prepareUpdate: Task.async(function*(aManifest, aZipPath) { + if (this._dryRun) { + return; + } + + this._setData(aManifest); + + let [ oldUniqueName, installDir ] = WebappOSUtils.getLaunchTarget(this.app); + if (!installDir) { + throw ERR_NOT_INSTALLED; + } + + if (this.uniqueName != oldUniqueName) { + // Bug 919799: If the app is still in the registry, migrate its data to + // the new format. + throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME; + } + + let updateDir = OS.Path.join(installDir, "update"); + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); + yield OS.File.makeDir(updateDir); + + try { + yield this._createDirectoryStructure(updateDir); + this._copyPrebuiltFiles(updateDir); + yield this._createConfigFiles(updateDir); + + if (aZipPath) { + yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile)); + } + + yield this._getIcon(updateDir); + } catch (ex) { + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); + throw ex; + } + }), + + /** + * Applies an update. + */ + applyUpdate: Task.async(function*() { + if (this._dryRun) { + return; + } + + let installDir = WebappOSUtils.getInstallPath(this.app); + let updateDir = OS.Path.join(installDir, "update"); + + let backupDir = yield this._backupInstallation(installDir); + + try { + // Move the update directory to the /Applications directory + yield this._applyTempInstallation(updateDir, installDir); + } catch (ex) { + yield this._restoreInstallation(backupDir, installDir); + throw ex; + } finally { + yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); + yield OS.File.removeDir(updateDir, { ignoreAbsent: true }); + } + }), + + _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) { + yield OS.File.move(OS.Path.join(aTmpDir, this.configJson), + OS.Path.join(this.appProfileDir, this.configJson)); + + yield moveDirectory(aTmpDir, aInstallDir); + }), + + _removeInstallation: function(keepProfile, aInstallDir) { + let filesToRemove = [ aInstallDir ]; + + if (!keepProfile) { + filesToRemove.push(this.appProfileDir); + } + + removeFiles(filesToRemove); + }, + + _backupInstallation: Task.async(function*(aInstallDir) { + let backupDir = OS.Path.join(aInstallDir, "backup"); + yield OS.File.removeDir(backupDir, { ignoreAbsent: true }); + yield OS.File.makeDir(backupDir); + + yield moveDirectory(OS.Path.join(aInstallDir, this.contentsDir), + backupDir); + yield OS.File.move(OS.Path.join(this.appProfileDir, this.configJson), + OS.Path.join(backupDir, this.configJson)); + + return backupDir; + }), + + _restoreInstallation: Task.async(function*(aBackupDir, aInstallDir) { + yield OS.File.move(OS.Path.join(aBackupDir, this.configJson), + OS.Path.join(this.appProfileDir, this.configJson)); + yield moveDirectory(aBackupDir, + OS.Path.join(aInstallDir, this.contentsDir)); + }), + + _createDirectoryStructure: Task.async(function*(aDir) { + yield OS.File.makeDir(this.appProfileDir, + { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); + + yield OS.File.makeDir(OS.Path.join(aDir, this.contentsDir), + { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); + + yield OS.File.makeDir(OS.Path.join(aDir, this.macOSDir), + { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); + + yield OS.File.makeDir(OS.Path.join(aDir, this.resourcesDir), + { unixMode: PERMS_DIRECTORY, ignoreExisting: true }); + }), + + _copyPrebuiltFiles: function(aDir) { + let destDir = getFile(aDir, this.macOSDir); + let stub = getFile(this.runtimeFolder, "webapprt-stub"); + stub.copyTo(destDir, "webapprt"); + }, + + _createConfigFiles: function(aDir) { + // ${ProfileDir}/webapp.json + yield writeToFile(OS.Path.join(aDir, this.configJson), + JSON.stringify(this.webappJson)); + + // ${InstallDir}/Contents/MacOS/webapp.ini + let applicationINI = getFile(aDir, this.macOSDir, "webapp.ini"); + + let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]. + getService(Ci.nsIINIParserFactory). + createINIParser(applicationINI). + QueryInterface(Ci.nsIINIParserWriter); + writer.setString("Webapp", "Name", this.appName); + writer.setString("Webapp", "Profile", this.uniqueName); + writer.writeFile(); + applicationINI.permissions = PERMS_FILE; + + // ${InstallDir}/Contents/Info.plist + let infoPListContent = '\n\ +\n\ +\n\ + \n\ + CFBundleDevelopmentRegion\n\ + English\n\ + CFBundleDisplayName\n\ + ' + escapeXML(this.appName) + '\n\ + CFBundleExecutable\n\ + webapprt\n\ + CFBundleIconFile\n\ + appicon\n\ + CFBundleIdentifier\n\ + ' + escapeXML(this.uniqueName) + '\n\ + CFBundleInfoDictionaryVersion\n\ + 6.0\n\ + CFBundleName\n\ + ' + escapeXML(this.appName) + '\n\ + CFBundlePackageType\n\ + APPL\n\ + CFBundleVersion\n\ + 0\n\ + NSHighResolutionCapable\n\ + \n\ + NSPrincipalClass\n\ + GeckoNSApplication\n\ + FirefoxBinary\n\ +#expand __MOZ_MACBUNDLE_ID__\n\ + \n\ +'; + + yield writeToFile(OS.Path.join(aDir, this.contentsDir, "Info.plist"), + infoPListContent); + }, + + /** + * Process the icon from the imageStream as retrieved from + * the URL by getIconForApp(). This will bundle the icon to the + * app package at Contents/Resources/appicon.icns. + * + * @param aMimeType the icon mimetype + * @param aImageStream the stream for the image data + * @param aDir the directory where the icon should be stored + */ + _processIcon: function(aMimeType, aIcon, aDir) { + let deferred = Promise.defer(); + + function conversionDone(aSubject, aTopic) { + if (aTopic == "process-finished") { + deferred.resolve(); + } else { + deferred.reject("Failure converting icon, exit code: " + aSubject.exitValue); + } + } + + let process = Cc["@mozilla.org/process/util;1"]. + createInstance(Ci.nsIProcess); + let sipsFile = getFile("/usr/bin/sips"); + + process.init(sipsFile); + process.runAsync(["-s", "format", "icns", + aIcon.path, + "--out", OS.Path.join(aDir, this.iconFile), + "-z", "128", "128"], + 9, conversionDone); + + return deferred.promise; + } +}