import * as fs from 'fs'; import * as path from 'path'; import * as log from 'loglevel'; import { NativefierOptions, RawOptions, } from '../../../shared/src/options/model'; import { dirExists, fileExists } from '../fsHelpers'; import { extractBoolean, extractString } from './plistInfoXMLHelpers'; import { getOptionsFromExecutable } from './executableHelpers'; import { parseJson } from '../../utils/parseUtils'; export type UpgradeAppInfo = { appResourcesDir: string; options: NativefierOptions; }; function findUpgradeAppResourcesDir(searchDir: string): string | null { searchDir = dirExists(searchDir) ? searchDir : path.dirname(searchDir); log.debug(`Searching for nativfier.json in ${searchDir}`); const children = fs.readdirSync(searchDir, { withFileTypes: true }); if (fileExists(path.join(searchDir, 'nativefier.json'))) { // Found 'nativefier.json', so this must be it! return path.resolve(searchDir); } const childDirectories = children.filter((c) => c.isDirectory()); for (const childDir of childDirectories) { // We must go deeper! const result = findUpgradeAppResourcesDir( path.join(searchDir, childDir.name, 'nativefier.json'), ); if (result !== null) { return path.resolve(result); } } // Didn't find it down here return null; } function getIconPath(appResourcesDir: string): string | undefined { const icnsPath = path.join(appResourcesDir, '..', 'electron.icns'); if (fileExists(icnsPath)) { log.debug(`Found icon at: ${icnsPath}`); return path.resolve(icnsPath); } const icoPath = path.join(appResourcesDir, 'icon.ico'); if (fileExists(icoPath)) { log.debug(`Found icon at: ${icoPath}`); return path.resolve(icoPath); } const pngPath = path.join(appResourcesDir, 'icon.png'); if (fileExists(pngPath)) { log.debug(`Found icon at: ${pngPath}`); return path.resolve(pngPath); } log.debug('Could not find icon file.'); return undefined; } function getInfoPListOptions( appResourcesDir: string, priorOptions: NativefierOptions, ): NativefierOptions { if (!fileExists(path.join(appResourcesDir, '..', '..', 'Info.plist'))) { // Not a darwin/mas app, so this is irrelevant return priorOptions; } const newOptions = { ...priorOptions }; const infoPlistXML: string = fs .readFileSync(path.join(appResourcesDir, '..', '..', 'Info.plist')) .toString(); if (newOptions.appCopyright === undefined) { // https://github.com/electron/electron-packager/blob/0d3f84374e9ab3741b171610735ebc6be3e5e75f/src/mac.js#L230-L232 newOptions.appCopyright = extractString( infoPlistXML, 'NSHumanReadableCopyright', ); log.debug( `Extracted app copyright from Info.plist: ${ newOptions.appCopyright as string }`, ); } if (newOptions.appVersion === undefined) { // https://github.com/electron/electron-packager/blob/0d3f84374e9ab3741b171610735ebc6be3e5e75f/src/mac.js#L214-L216 // This could also be the buildVersion, but since they end up in the same place, that SHOULDN'T matter const bundleVersion = extractString(infoPlistXML, 'CFBundleVersion'); newOptions.appVersion = bundleVersion === undefined || bundleVersion === '1.0.0' // If it's 1.0.0, that's just the default ? undefined : bundleVersion; (newOptions.darwinDarkModeSupport = newOptions.darwinDarkModeSupport === undefined ? undefined : newOptions.darwinDarkModeSupport === false), log.debug( `Extracted app version from Info.plist: ${ newOptions.appVersion as string }`, ); } if (newOptions.darwinDarkModeSupport === undefined) { // https://github.com/electron/electron-packager/blob/0d3f84374e9ab3741b171610735ebc6be3e5e75f/src/mac.js#L234-L236 newOptions.darwinDarkModeSupport = extractBoolean( infoPlistXML, 'NSRequiresAquaSystemAppearance', ); log.debug( `Extracted Darwin dark mode support from Info.plist: ${ newOptions.darwinDarkModeSupport ? 'Yes' : 'No' }`, ); } return newOptions; } function getInjectPaths(appResourcesDir: string): string[] | undefined { const injectDir = path.join(appResourcesDir, 'inject'); if (!dirExists(injectDir)) { return undefined; } const injectPaths = fs .readdirSync(injectDir, { withFileTypes: true }) .filter( (fd) => fd.isFile() && (fd.name.toLowerCase().endsWith('.css') || fd.name.toLowerCase().endsWith('.js')), ) .map((fd) => path.resolve(path.join(injectDir, fd.name))); log.debug(`CSS/JS Inject paths: ${injectPaths.join(', ')}`); return injectPaths; } function isAsar(appResourcesDir: string): boolean { const asar = fileExists(path.join(appResourcesDir, '..', 'electron.asar')); log.debug(`Is this app an ASAR? ${asar ? 'Yes' : 'No'}`); return asar; } export function findUpgradeApp(upgradeFrom: string): UpgradeAppInfo | null { const searchDir = dirExists(upgradeFrom) ? upgradeFrom : path.dirname(upgradeFrom); log.debug(`Looking for old options file in ${searchDir}`); const appResourcesDir = findUpgradeAppResourcesDir(searchDir); if (appResourcesDir === null) { log.debug(`No nativefier.json file found in ${searchDir}`); return null; } const nativefierJSONPath = path.join(appResourcesDir, 'nativefier.json'); log.debug(`Loading ${nativefierJSONPath}`); const options = parseJson( fs.readFileSync(nativefierJSONPath, 'utf8'), ); if (!options) { throw new Error( `Could not read Nativefier options from ${nativefierJSONPath}`, ); } options.electronVersion = undefined; return { appResourcesDir, options: { ...options, ...getOptionsFromExecutable(appResourcesDir, options), ...getInfoPListOptions(appResourcesDir, options), asar: options.asar !== undefined ? options.asar : isAsar(appResourcesDir), icon: getIconPath(appResourcesDir), inject: getInjectPaths(appResourcesDir), }, }; } export function useOldAppOptions( rawOptions: RawOptions, oldApp: UpgradeAppInfo, ): RawOptions { if (rawOptions.targetUrl !== undefined && dirExists(rawOptions.targetUrl)) { // You got your ouput dir in my targetUrl! rawOptions.out = rawOptions.targetUrl; } log.debug('rawOptions', rawOptions); log.debug('oldApp', oldApp); const combinedOptions = { ...rawOptions, ...oldApp.options }; log.debug('Combined options', combinedOptions); return combinedOptions; }