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