2
2
mirror of https://github.com/Llewellynvdm/nativefier.git synced 2024-12-23 18:48:55 +00:00

Add an option to upgrade an existing app (fix #1131) (PR #1138)

This adds a `--upgrade` option to upgrade-in-place an old app, re-using its options it can.
Should help fix #1131

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
This commit is contained in:
Adam Weeden 2021-04-28 22:00:31 -04:00 committed by GitHub
parent bc6be8445d
commit 9330c8434f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 701 additions and 39 deletions

View File

@ -9,6 +9,7 @@
- [[dest]](#dest) - [[dest]](#dest)
- [Help](#help) - [Help](#help)
- [Version](#version) - [Version](#version)
- [[upgrade]](#upgrade)
- [[name]](#name) - [[name]](#name)
- [[platform]](#platform) - [[platform]](#platform)
- [[arch]](#arch) - [[arch]](#arch)
@ -92,9 +93,13 @@ See [PR #744 - Support packaging nativefier applications into Squirrel-based ins
## Command Line ## Command Line
```bash ```bash
nativefier [options] <targetUrl> [dest] nativefier [options] [targetUrl] [dest]
``` ```
You must provide:
- Either a `targetUrl` to generate a new app from it.
- Or option `--upgrade <pathOfAppToUpgrade>` to upgrade an existing app.
Command line options are listed below. Command line options are listed below.
#### Target Url #### Target Url
@ -121,6 +126,22 @@ Prints the usage information.
Prints the version of your `nativefier` install. Prints the version of your `nativefier` install.
#### [upgrade]
```
--upgrade <pathToExistingApp>
```
*NEW IN 43.1.0*
This option will attempt to extract all existing options from the old app, and upgrade it using the current Nativefier CLI.
**IMPORTANT NOTE**
**This action is an in-place upgrade, and will REPLACE the current application. In case this feature does not work as intended or as the user may wish, it is advised to make a backup of the app to be upgraded before using, or specify an alternate directory as you would when creating a new file.**
The provided path must be the "executable" of an application packaged with a previous version of Nativefier, and to be upgraded to the latest version of Nativefier. "Executable" means: the `.exe` file on Windows, the executable on Linux, or the `.app` on macOS. The executable must be living in the original context where it was generated (i.e., on Windows and Linux, the exe file must still be in the folder containing the generated `resources` directory).
#### [name] #### [name]
``` ```

View File

@ -12,7 +12,8 @@ import {
isWindows, isWindows,
isWindowsAdmin, isWindowsAdmin,
} from '../helpers/helpers'; } from '../helpers/helpers';
import { AppOptions, NativefierOptions } from '../options/model'; import { useOldAppOptions, findUpgradeApp } from '../helpers/upgrade/upgrade';
import { AppOptions } from '../options/model';
import { getOptions } from '../options/optionsMain'; import { getOptions } from '../options/optionsMain';
import { prepareElectronApp } from './prepareElectronApp'; import { prepareElectronApp } from './prepareElectronApp';
@ -25,28 +26,6 @@ const OPTIONS_REQUIRING_WINDOWS_FOR_WINDOWS_BUILD = [
'win32metadata', 'win32metadata',
]; ];
/**
* Checks the app path array to determine if packaging completed successfully
*/
function getAppPath(appPath: string | string[]): string {
if (!Array.isArray(appPath)) {
return appPath;
}
if (appPath.length === 0) {
return null; // directory already exists and `--overwrite` not set
}
if (appPath.length > 1) {
log.warn(
'Warning: This should not be happening, packaged app path contains more than one element:',
appPath,
);
}
return appPath[0];
}
/** /**
* For Windows & Linux, we have to copy over the icon to the resources/app * For Windows & Linux, we have to copy over the icon to the resources/app
* folder, which the BrowserWindow is hard-coded to read the icon from * folder, which the BrowserWindow is hard-coded to read the icon from
@ -88,6 +67,36 @@ async function copyIconsIfNecessary(
await copyFileOrDir(options.packager.icon, destIconPath); await copyFileOrDir(options.packager.icon, destIconPath);
} }
/**
* Checks the app path array to determine if packaging completed successfully
*/
function getAppPath(appPath: string | string[]): string {
if (!Array.isArray(appPath)) {
return appPath;
}
if (appPath.length === 0) {
return null; // directory already exists and `--overwrite` not set
}
if (appPath.length > 1) {
log.warn(
'Warning: This should not be happening, packaged app path contains more than one element:',
appPath,
);
}
return appPath[0];
}
function isUpgrade(rawOptions) {
return (
rawOptions.upgrade !== undefined &&
(rawOptions.upgrade === true ||
(typeof rawOptions.upgrade === 'string' && rawOptions.upgrade !== ''))
);
}
function trimUnprocessableOptions(options: AppOptions): void { function trimUnprocessableOptions(options: AppOptions): void {
if ( if (
options.packager.platform === 'win32' && options.packager.platform === 'win32' &&
@ -116,11 +125,28 @@ function trimUnprocessableOptions(options: AppOptions): void {
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function buildNativefierApp( export async function buildNativefierApp(rawOptions): Promise<string> {
rawOptions: NativefierOptions, log.info('\nProcessing options...');
): Promise<string> {
log.info('Processing options...'); if (isUpgrade(rawOptions)) {
log.debug('Attempting to upgrade from', rawOptions.upgrade);
const oldApp = findUpgradeApp(rawOptions.upgrade.toString());
if (oldApp === null) {
throw new Error(
`Could not find an old Nativfier app in "${
rawOptions.upgrade as string
}"`,
);
}
rawOptions = useOldAppOptions(rawOptions, oldApp);
if (rawOptions.out === undefined && rawOptions.overwrite) {
rawOptions.out = path.dirname(rawOptions.upgrade);
}
}
log.debug('rawOptions', rawOptions);
const options = await getOptions(rawOptions); const options = await getOptions(rawOptions);
log.debug('options', options);
if (options.packager.platform === 'darwin' && isWindows()) { if (options.packager.platform === 'darwin' && isWindows()) {
// electron-packager has to extract the desired electron package for the target platform. // electron-packager has to extract the desired electron package for the target platform.

View File

@ -17,58 +17,76 @@ function pickElectronAppArgs(options: AppOptions): any {
return { return {
accessibilityPrompt: options.nativefier.accessibilityPrompt, accessibilityPrompt: options.nativefier.accessibilityPrompt,
alwaysOnTop: options.nativefier.alwaysOnTop, alwaysOnTop: options.nativefier.alwaysOnTop,
appBundleId: options.packager.appBundleId,
appCategoryType: options.packager.appCategoryType,
appCopyright: options.packager.appCopyright, appCopyright: options.packager.appCopyright,
appVersion: options.packager.appVersion, appVersion: options.packager.appVersion,
arch: options.packager.arch,
asar: options.packager.asar,
backgroundColor: options.nativefier.backgroundColor, backgroundColor: options.nativefier.backgroundColor,
basicAuthPassword: options.nativefier.basicAuthPassword, basicAuthPassword: options.nativefier.basicAuthPassword,
basicAuthUsername: options.nativefier.basicAuthUsername, basicAuthUsername: options.nativefier.basicAuthUsername,
blockExternalUrls: options.nativefier.blockExternalUrls,
bounce: options.nativefier.bounce, bounce: options.nativefier.bounce,
browserwindowOptions: options.nativefier.browserwindowOptions, browserwindowOptions: options.nativefier.browserwindowOptions,
buildDate: new Date().getTime(),
buildVersion: options.packager.buildVersion, buildVersion: options.packager.buildVersion,
clearCache: options.nativefier.clearCache, clearCache: options.nativefier.clearCache,
counter: options.nativefier.counter, counter: options.nativefier.counter,
crashReporter: options.nativefier.crashReporter, crashReporter: options.nativefier.crashReporter,
darwinDarkModeSupport: options.packager.darwinDarkModeSupport, darwinDarkModeSupport: options.packager.darwinDarkModeSupport,
derefSymlinks: options.packager.derefSymlinks,
disableContextMenu: options.nativefier.disableContextMenu, disableContextMenu: options.nativefier.disableContextMenu,
disableDevTools: options.nativefier.disableDevTools, disableDevTools: options.nativefier.disableDevTools,
disableGpu: options.nativefier.disableGpu, disableGpu: options.nativefier.disableGpu,
disableOldBuildWarning: options.nativefier.disableOldBuildWarning,
diskCacheSize: options.nativefier.diskCacheSize, diskCacheSize: options.nativefier.diskCacheSize,
download: options.packager.download,
electronVersionUsed: options.packager.electronVersion,
enableEs3Apis: options.nativefier.enableEs3Apis, enableEs3Apis: options.nativefier.enableEs3Apis,
executableName: options.packager.executableName,
fastQuit: options.nativefier.fastQuit, fastQuit: options.nativefier.fastQuit,
fileDownloadOptions: options.nativefier.fileDownloadOptions, fileDownloadOptions: options.nativefier.fileDownloadOptions,
flashPluginDir: options.nativefier.flashPluginDir, flashPluginDir: options.nativefier.flashPluginDir,
fullScreen: options.nativefier.fullScreen, fullScreen: options.nativefier.fullScreen,
globalShortcuts: options.nativefier.globalShortcuts, globalShortcuts: options.nativefier.globalShortcuts,
height: options.nativefier.height, height: options.nativefier.height,
helperBundleId: options.packager.helperBundleId,
hideWindowFrame: options.nativefier.hideWindowFrame, hideWindowFrame: options.nativefier.hideWindowFrame,
ignoreCertificate: options.nativefier.ignoreCertificate, ignoreCertificate: options.nativefier.ignoreCertificate,
ignoreGpuBlacklist: options.nativefier.ignoreGpuBlacklist, ignoreGpuBlacklist: options.nativefier.ignoreGpuBlacklist,
insecure: options.nativefier.insecure, insecure: options.nativefier.insecure,
internalUrls: options.nativefier.internalUrls, internalUrls: options.nativefier.internalUrls,
blockExternalUrls: options.nativefier.blockExternalUrls, isUpgrade: options.packager.upgrade,
maxHeight: options.nativefier.maxHeight, junk: options.packager.junk,
maximize: options.nativefier.maximize, maximize: options.nativefier.maximize,
maxHeight: options.nativefier.maxHeight,
maxWidth: options.nativefier.maxWidth, maxWidth: options.nativefier.maxWidth,
minHeight: options.nativefier.minHeight, minHeight: options.nativefier.minHeight,
minWidth: options.nativefier.minWidth, minWidth: options.nativefier.minWidth,
name: options.packager.name, name: options.packager.name,
nativefierVersion: options.nativefier.nativefierVersion, nativefierVersion: options.nativefier.nativefierVersion,
osxNotarize: options.packager.osxNotarize,
osxSign: options.packager.osxSign,
processEnvs: options.nativefier.processEnvs, processEnvs: options.nativefier.processEnvs,
protocols: options.packager.protocols,
proxyRules: options.nativefier.proxyRules, proxyRules: options.nativefier.proxyRules,
prune: options.packager.prune,
quiet: options.packager.quiet,
showMenuBar: options.nativefier.showMenuBar, showMenuBar: options.nativefier.showMenuBar,
singleInstance: options.nativefier.singleInstance, singleInstance: options.nativefier.singleInstance,
targetUrl: options.packager.targetUrl, targetUrl: options.packager.targetUrl,
titleBarStyle: options.nativefier.titleBarStyle, titleBarStyle: options.nativefier.titleBarStyle,
tray: options.nativefier.tray, tray: options.nativefier.tray,
usageDescription: options.packager.usageDescription,
userAgent: options.nativefier.userAgent, userAgent: options.nativefier.userAgent,
userAgentOverriden: options.nativefier.userAgentOverriden,
versionString: options.nativefier.versionString, versionString: options.nativefier.versionString,
width: options.nativefier.width, width: options.nativefier.width,
win32metadata: options.packager.win32metadata, win32metadata: options.packager.win32metadata,
disableOldBuildWarning: options.nativefier.disableOldBuildWarning,
x: options.nativefier.x, x: options.nativefier.x,
y: options.nativefier.y, y: options.nativefier.y,
zoom: options.nativefier.zoom, zoom: options.nativefier.zoom,
buildDate: new Date().getTime(),
// OLD_BUILD_WARNING_TEXT is an undocumented env. var to let *packagers* // OLD_BUILD_WARNING_TEXT is an undocumented env. var to let *packagers*
// tweak the message shown on warning about an old build, to something // tweak the message shown on warning about an old build, to something
// more tailored to their audience (who might not even know Nativefier). // more tailored to their audience (who might not even know Nativefier).

View File

@ -9,6 +9,7 @@ import * as log from 'loglevel';
import { isArgFormatInvalid } from './helpers/helpers'; import { isArgFormatInvalid } from './helpers/helpers';
import { supportedArchs, supportedPlatforms } from './infer/inferOs'; import { supportedArchs, supportedPlatforms } from './infer/inferOs';
import { buildNativefierApp } from './main'; import { buildNativefierApp } from './main';
import { NativefierOptions } from './options/model';
import { parseBooleanOrString, parseJson } from './utils/parseUtils'; import { parseBooleanOrString, parseJson } from './utils/parseUtils';
// package.json is `require`d to let tsc strip the `src` folder by determining // package.json is `require`d to let tsc strip the `src` folder by determining
@ -67,12 +68,16 @@ if (require.main === module) {
const args = commander const args = commander
.name('nativefier') .name('nativefier')
.version(packageJson.version, '-v, --version') .version(packageJson.version, '-v, --version')
.arguments('<targetUrl> [dest]') .arguments('[targetUrl] [dest]')
.action((url, outputDirectory) => { .action((url, outputDirectory) => {
positionalOptions.targetUrl = url; positionalOptions.targetUrl = url;
positionalOptions.out = outputDirectory; positionalOptions.out = outputDirectory;
}) })
.option('-n, --name <value>', 'app name') .option('-n, --name <value>', 'app name')
.option(
'--upgrade <pathToExistingApp>',
'Upgrade an app built by an older Nativefier. You must pass the full path to the existing app executable (app will be overwritten with upgraded version by default)',
)
.addOption( .addOption(
new commander.Option('-p, --platform <value>').choices( new commander.Option('-p, --platform <value>').choices(
supportedPlatforms, supportedPlatforms,
@ -291,7 +296,18 @@ if (require.main === module) {
commander.help(); commander.help();
} }
checkInternet(); checkInternet();
const options = { ...positionalOptions, ...commander.opts() }; const options: NativefierOptions = {
...positionalOptions,
...commander.opts(),
};
if (!options.targetUrl && !options.upgrade) {
console.error(
'Nativefier must be called with either a targetUrl or the --upgrade option.',
);
commander.help();
}
buildNativefierApp(options).catch((error) => { buildNativefierApp(options).catch((error) => {
log.error('Error during build. Run with --verbose for details.', error); log.error('Error during build. Run with --verbose for details.', error);
}); });

19
src/helpers/fsHelpers.ts Normal file
View File

@ -0,0 +1,19 @@
import * as fs from 'fs';
export function dirExists(dirName: string): boolean {
try {
const dirStat = fs.statSync(dirName);
return dirStat.isDirectory();
} catch {
return false;
}
}
export function fileExists(fileName: string): boolean {
try {
const fileStat = fs.statSync(fileName);
return fileStat.isFile();
} catch {
return false;
}
}

View File

@ -0,0 +1,186 @@
import * as fs from 'fs';
import * as path from 'path';
import * as log from 'loglevel';
import { NativefierOptions } from '../../options/model';
import { getVersionString } from './rceditGet';
import { fileExists } from '../fsHelpers';
type ExecutableInfo = {
arch?: string;
};
function getExecutableBytes(executablePath: string): Uint8Array {
return fs.readFileSync(executablePath);
}
function getExecutableArch(
exeBytes: Uint8Array,
platform: string,
): string | undefined {
switch (platform) {
case 'linux':
// https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
switch (exeBytes[0x12]) {
case 0x03:
return 'ia32';
case 0x28:
return 'armv7l';
case 0x3e:
return 'x64';
case 0xb7:
return 'arm64';
default:
return undefined;
}
case 'darwin':
case 'mas':
// https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h
switch ((exeBytes[0x04] << 8) + exeBytes[0x05]) {
case 0x0700:
return 'x64';
case 0x0c00:
return 'arm64';
default:
return undefined;
}
case 'windows':
// https://en.wikibooks.org/wiki/X86_Disassembly/Windows_Executable_Files#COFF_Header
switch ((exeBytes[0x7d] << 8) + exeBytes[0x7c]) {
case 0x014c:
return 'ia32';
case 0x8664:
return 'x64';
case 0xaa64:
return 'arm64';
default:
return undefined;
}
default:
return undefined;
}
}
function getExecutableInfo(
executablePath: string,
platform: string,
): ExecutableInfo {
if (!fileExists(executablePath)) {
return {};
}
const exeBytes = getExecutableBytes(executablePath);
return {
arch: getExecutableArch(exeBytes, platform),
};
}
export function getOptionsFromExecutable(
appResourcesDir: string,
priorOptions: NativefierOptions,
): NativefierOptions {
const newOptions: NativefierOptions = { ...priorOptions };
let executablePath: string | undefined = undefined;
const appRoot = path.resolve(path.join(appResourcesDir, '..', '..'));
const children = fs.readdirSync(appRoot, { withFileTypes: true });
const looksLikeMacOS =
children.filter((c) => c.name === 'MacOS' && c.isDirectory()).length > 0;
const looksLikeWindows =
children.filter((c) => c.name.toLowerCase().endsWith('.exe') && c.isFile())
.length > 0;
const looksLikeLinux =
children.filter((c) => c.name.toLowerCase().endsWith('.so') && c.isFile())
.length > 0;
if (looksLikeMacOS) {
log.debug('This looks like a MacOS app...');
if (newOptions.platform === undefined) {
newOptions.platform =
children.filter((c) => c.name === 'Library' && c.isDirectory()).length >
0
? 'mas'
: 'darwin';
}
executablePath = path.join(
appRoot,
'MacOS',
fs.readdirSync(path.join(appRoot, 'MacOS'))[0],
);
} else if (looksLikeWindows) {
log.debug('This looks like a Windows app...');
if (newOptions.platform === undefined) {
newOptions.platform = 'windows';
}
executablePath = path.join(
appRoot,
children.filter(
(c) =>
c.name.toLowerCase() === `${newOptions.name.toLowerCase()}.exe` &&
c.isFile(),
)[0].name,
);
if (newOptions.appVersion === undefined) {
// https://github.com/electron/electron-packager/blob/f1c159f4c844d807968078ea504fba40ca7d9c73/src/win32.js#L46-L48
newOptions.appVersion = getVersionString(
executablePath,
'ProductVersion',
);
log.debug(
`Extracted app version from executable: ${newOptions.appVersion}`,
);
}
if (newOptions.buildVersion === undefined) {
//https://github.com/electron/electron-packager/blob/f1c159f4c844d807968078ea504fba40ca7d9c73/src/win32.js#L50-L52
newOptions.buildVersion = getVersionString(executablePath, 'FileVersion');
if (newOptions.appVersion == newOptions.buildVersion) {
newOptions.buildVersion = undefined;
} else {
log.debug(
`Extracted build version from executable: ${newOptions.buildVersion}`,
);
}
}
if (newOptions.appCopyright === undefined) {
// https://github.com/electron/electron-packager/blob/f1c159f4c844d807968078ea504fba40ca7d9c73/src/win32.js#L54-L56
newOptions.appCopyright = getVersionString(
executablePath,
'LegalCopyright',
);
log.debug(
`Extracted app copyright from executable: ${newOptions.appCopyright}`,
);
}
} else if (looksLikeLinux) {
log.debug('This looks like a Linux app...');
if (newOptions.platform === undefined) {
newOptions.platform = 'linux';
}
executablePath = path.join(
appRoot,
children.filter((c) => c.name == newOptions.name && c.isFile())[0].name,
);
}
log.debug(`Executable path: ${executablePath}`);
if (newOptions.arch === undefined) {
const executableInfo = getExecutableInfo(
executablePath,
newOptions.platform,
);
newOptions.arch = executableInfo.arch;
log.debug(`Extracted arch from executable: ${newOptions.arch}`);
}
if (newOptions.platform === undefined || newOptions.arch == undefined) {
throw Error(`Could not determine platform / arch of app in ${appRoot}`);
}
return newOptions;
}

View File

@ -0,0 +1,39 @@
export function extractBoolean(
infoPlistXML: string,
plistKey: string,
): boolean | undefined {
const plistValue = extractRaw(infoPlistXML, plistKey);
return plistValue === undefined
? undefined
: plistValue.split('<')[1].split('/>')[0].toLowerCase() === 'true';
}
export function extractString(
infoPlistXML: string,
plistKey: string,
): string | undefined {
const plistValue = extractRaw(infoPlistXML, plistKey);
return plistValue === undefined
? undefined
: plistValue.split('<string>')[1].split('</string>')[0];
}
function extractRaw(
infoPlistXML: string,
plistKey: string,
): string | undefined {
// This would be easier with xml2js, but let's not add a dependency for something this minor.
const fullKey = `\n <key>${plistKey}</key>`;
if (infoPlistXML.indexOf(fullKey) === -1) {
// This value wasn't set, so we'll stay agnostic to it
return undefined;
}
return infoPlistXML
.split(fullKey)[1]
.split('\n </dict>')[0] // Get everything between here and the end of the main plist dict
.split('\n <key>')[0]; // Get everything before the next key (if it exists)
}

View File

@ -0,0 +1,42 @@
import * as os from 'os';
import * as path from 'path';
import { spawnSync } from 'child_process';
// A modification of https://github.com/electron/node-rcedit to support the retrieval
// of information.
export function getVersionString(
executablePath: string,
versionString: string,
): string {
let rcedit = path.resolve(
__dirname,
'..',
'..',
'..',
'node_modules',
'rcedit',
'bin',
process.arch === 'x64' ? 'rcedit-x64.exe' : 'rcedit.exe',
);
const args = [executablePath, `--get-version-string`, versionString];
const spawnOptions = {
env: { ...process.env },
};
// Use Wine on non-Windows platforms except for WSL, which doesn't need it
if (process.platform !== 'win32' && !os.release().endsWith('Microsoft')) {
args.unshift(rcedit);
rcedit = process.arch === 'x64' ? 'wine64' : 'wine';
// Suppress "fixme:" stderr log messages
spawnOptions.env.WINEDEBUG = '-all';
}
try {
const child = spawnSync(rcedit, args, spawnOptions);
const result = child.output?.toString().split(',wine: ')[0];
return result.startsWith(',') ? result.substr(1) : result;
} catch {
return undefined;
}
}

View File

@ -0,0 +1,199 @@
import * as fs from 'fs';
import * as path from 'path';
import * as log from 'loglevel';
import { NativefierOptions } from '../../options/model';
import { dirExists, fileExists } from '../fsHelpers';
import { extractBoolean, extractString } from './plistInfoXMLHelpers';
import { getOptionsFromExecutable } from './executableHelpers';
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}`,
);
}
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}`,
);
}
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;
}
log.debug(`Loading ${path.join(appResourcesDir, 'nativefier.json')}`);
const options: NativefierOptions = JSON.parse(
fs.readFileSync(path.join(appResourcesDir, 'nativefier.json'), 'utf8'),
);
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: NativefierOptions,
oldApp: UpgradeAppInfo,
): NativefierOptions {
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);
if (
oldApp.options.userAgentOverriden === undefined ||
oldApp.options.userAgentOverriden === false
) {
oldApp.options.userAgent = undefined;
}
const combinedOptions = { ...rawOptions, ...oldApp.options };
log.debug('Combined options', combinedOptions);
return combinedOptions;
}

View File

@ -1,10 +1,24 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { DEFAULT_ELECTRON_VERSION } from './constants';
import { getTempDir } from './helpers/helpers'; import { getTempDir } from './helpers/helpers';
import { inferArch } from './infer/inferOs';
import { inferUserAgent } from './infer/inferUserAgent';
import { buildNativefierApp } from './main'; import { buildNativefierApp } from './main';
function checkApp(appRoot: string, inputOptions: any): void { async function checkApp(appRoot: string, inputOptions: any): Promise<void> {
const arch = (inputOptions.arch as string) || inferArch();
if (inputOptions.out !== undefined) {
expect(
path.join(
inputOptions.out,
`Google-${inputOptions.platform as string}-${arch}`,
),
).toBe(appRoot);
}
let relativeAppFolder: string; let relativeAppFolder: string;
switch (inputOptions.platform) { switch (inputOptions.platform) {
@ -18,7 +32,9 @@ function checkApp(appRoot: string, inputOptions: any): void {
relativeAppFolder = 'resources/app'; relativeAppFolder = 'resources/app';
break; break;
default: default:
throw new Error('Unknown app platform'); throw new Error(
`Unknown app platform: ${new String(inputOptions.platform).toString()}`,
);
} }
const appPath = path.join(appRoot, relativeAppFolder); const appPath = path.join(appRoot, relativeAppFolder);
@ -36,6 +52,28 @@ function checkApp(appRoot: string, inputOptions: any): void {
const iconPath = path.join(appPath, iconFile); const iconPath = path.join(appPath, iconFile);
expect(fs.existsSync(iconPath)).toBe(true); expect(fs.existsSync(iconPath)).toBe(true);
expect(fs.statSync(iconPath).size).toBeGreaterThan(1000); expect(fs.statSync(iconPath).size).toBeGreaterThan(1000);
// Test arch
if (inputOptions.arch !== undefined) {
expect(inputOptions.arch).toBe(nativefierConfig.arch);
} else {
expect(os.arch()).toBe(nativefierConfig.arch);
}
// Test electron version
expect(nativefierConfig.electronVersionUsed).toBe(
inputOptions.electronVersion || DEFAULT_ELECTRON_VERSION,
);
// Test user agent
expect(nativefierConfig.userAgent).toBe(
inputOptions.userAgent !== undefined
? inputOptions.userAgent
: await inferUserAgent(
inputOptions.electronVersion || DEFAULT_ELECTRON_VERSION,
inputOptions.platform,
),
);
} }
describe('Nativefier', () => { describe('Nativefier', () => {
@ -52,7 +90,54 @@ describe('Nativefier', () => {
platform, platform,
}; };
const appPath = await buildNativefierApp(options); const appPath = await buildNativefierApp(options);
checkApp(appPath, options); await checkApp(appPath, options);
},
);
});
describe('Nativefier upgrade', () => {
jest.setTimeout(300000);
test.each([
{ platform: 'darwin', arch: 'x64' },
{ platform: 'linux', arch: 'arm64', userAgent: 'FIREFOX' },
// Exhaustive integration testing here would be neat, but takes too long.
// -> For now, only testing a subset of platforms/archs
// { platform: 'win32', arch: 'x64' },
// { platform: 'win32', arch: 'ia32' },
// { platform: 'darwin', arch: 'arm64' },
// { platform: 'linux', arch: 'x64' },
// { platform: 'linux', arch: 'armv7l' },
// { platform: 'linux', arch: 'ia32' },
])(
'can upgrade a Nativefier app for platform/arch: %s',
async (baseAppOptions) => {
const tempDirectory = getTempDir('integtestUpgrade1');
const options = {
targetUrl: 'https://google.com/',
out: tempDirectory,
overwrite: true,
electronVersion: '11.2.3',
...baseAppOptions,
};
const appPath = await buildNativefierApp(options);
await checkApp(appPath, options);
const upgradeOptions = {
upgrade: appPath,
overwrite: true,
};
const upgradeAppPath = await buildNativefierApp(upgradeOptions);
options.electronVersion = DEFAULT_ELECTRON_VERSION;
options.userAgent =
baseAppOptions.userAgent !== undefined
? baseAppOptions.userAgent
: await inferUserAgent(
DEFAULT_ELECTRON_VERSION,
baseAppOptions.platform,
);
await checkApp(upgradeAppPath, options);
}, },
); );
}); });

View File

@ -3,6 +3,8 @@ import * as electronPackager from 'electron-packager';
export interface ElectronPackagerOptions extends electronPackager.Options { export interface ElectronPackagerOptions extends electronPackager.Options {
targetUrl: string; targetUrl: string;
platform: string; platform: string;
upgrade: boolean;
upgradeFrom?: string;
} }
export interface AppOptions { export interface AppOptions {
@ -24,6 +26,7 @@ export interface AppOptions {
disableGpu: boolean; disableGpu: boolean;
disableOldBuildWarning: boolean; disableOldBuildWarning: boolean;
diskCacheSize: number; diskCacheSize: number;
electronVersionUsed?: string;
enableEs3Apis: boolean; enableEs3Apis: boolean;
fastQuit: boolean; fastQuit: boolean;
fileDownloadOptions: any; fileDownloadOptions: any;
@ -46,6 +49,7 @@ export interface AppOptions {
titleBarStyle: string; titleBarStyle: string;
tray: string | boolean; tray: string | boolean;
userAgent: string; userAgent: string;
userAgentOverriden: boolean;
verbose: boolean; verbose: boolean;
versionString: string; versionString: string;
width: number; width: number;

View File

@ -28,7 +28,7 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
appCopyright: rawOptions.appCopyright, appCopyright: rawOptions.appCopyright,
appVersion: rawOptions.appVersion, appVersion: rawOptions.appVersion,
arch: rawOptions.arch || inferArch(), arch: rawOptions.arch || inferArch(),
asar: rawOptions.conceal || false, asar: rawOptions.asar || rawOptions.conceal || false,
buildVersion: rawOptions.buildVersion, buildVersion: rawOptions.buildVersion,
darwinDarkModeSupport: rawOptions.darwinDarkModeSupport || false, darwinDarkModeSupport: rawOptions.darwinDarkModeSupport || false,
dir: PLACEHOLDER_APP_DIR, dir: PLACEHOLDER_APP_DIR,
@ -38,8 +38,13 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
out: rawOptions.out || process.cwd(), out: rawOptions.out || process.cwd(),
overwrite: rawOptions.overwrite, overwrite: rawOptions.overwrite,
platform: rawOptions.platform || inferPlatform(), platform: rawOptions.platform || inferPlatform(),
targetUrl: normalizeUrl(rawOptions.targetUrl), targetUrl:
rawOptions.targetUrl === undefined
? '' // We'll plug this in later via upgrade
: normalizeUrl(rawOptions.targetUrl),
tmpdir: false, // workaround for electron-packager#375 tmpdir: false, // workaround for electron-packager#375
upgrade: rawOptions.upgrade !== undefined ? true : false,
upgradeFrom: rawOptions.upgrade,
win32metadata: rawOptions.win32metadata || { win32metadata: rawOptions.win32metadata || {
ProductName: rawOptions.name, ProductName: rawOptions.name,
InternalName: rawOptions.name, InternalName: rawOptions.name,
@ -86,6 +91,8 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
titleBarStyle: rawOptions.titleBarStyle || null, titleBarStyle: rawOptions.titleBarStyle || null,
tray: rawOptions.tray || false, tray: rawOptions.tray || false,
userAgent: rawOptions.userAgent, userAgent: rawOptions.userAgent,
userAgentOverriden:
rawOptions.userAgent !== undefined && rawOptions.userAgent !== null,
verbose: rawOptions.verbose, verbose: rawOptions.verbose,
versionString: rawOptions.versionString, versionString: rawOptions.versionString,
width: rawOptions.width || 1280, width: rawOptions.width || 1280,