mirror of
https://github.com/Llewellynvdm/nativefier.git
synced 2024-12-23 02:28:55 +00:00
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:
parent
bc6be8445d
commit
9330c8434f
23
docs/api.md
23
docs/api.md
@ -9,6 +9,7 @@
|
||||
- [[dest]](#dest)
|
||||
- [Help](#help)
|
||||
- [Version](#version)
|
||||
- [[upgrade]](#upgrade)
|
||||
- [[name]](#name)
|
||||
- [[platform]](#platform)
|
||||
- [[arch]](#arch)
|
||||
@ -92,9 +93,13 @@ See [PR #744 - Support packaging nativefier applications into Squirrel-based ins
|
||||
## Command Line
|
||||
|
||||
```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.
|
||||
|
||||
#### Target Url
|
||||
@ -121,6 +126,22 @@ Prints the usage information.
|
||||
|
||||
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]
|
||||
|
||||
```
|
||||
|
@ -12,7 +12,8 @@ import {
|
||||
isWindows,
|
||||
isWindowsAdmin,
|
||||
} 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 { prepareElectronApp } from './prepareElectronApp';
|
||||
|
||||
@ -25,28 +26,6 @@ const OPTIONS_REQUIRING_WINDOWS_FOR_WINDOWS_BUILD = [
|
||||
'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
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (
|
||||
options.packager.platform === 'win32' &&
|
||||
@ -116,11 +125,28 @@ function trimUnprocessableOptions(options: AppOptions): void {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export async function buildNativefierApp(
|
||||
rawOptions: NativefierOptions,
|
||||
): Promise<string> {
|
||||
log.info('Processing options...');
|
||||
export async function buildNativefierApp(rawOptions): Promise<string> {
|
||||
log.info('\nProcessing 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);
|
||||
log.debug('options', options);
|
||||
|
||||
if (options.packager.platform === 'darwin' && isWindows()) {
|
||||
// electron-packager has to extract the desired electron package for the target platform.
|
||||
|
@ -17,58 +17,76 @@ function pickElectronAppArgs(options: AppOptions): any {
|
||||
return {
|
||||
accessibilityPrompt: options.nativefier.accessibilityPrompt,
|
||||
alwaysOnTop: options.nativefier.alwaysOnTop,
|
||||
appBundleId: options.packager.appBundleId,
|
||||
appCategoryType: options.packager.appCategoryType,
|
||||
appCopyright: options.packager.appCopyright,
|
||||
appVersion: options.packager.appVersion,
|
||||
arch: options.packager.arch,
|
||||
asar: options.packager.asar,
|
||||
backgroundColor: options.nativefier.backgroundColor,
|
||||
basicAuthPassword: options.nativefier.basicAuthPassword,
|
||||
basicAuthUsername: options.nativefier.basicAuthUsername,
|
||||
blockExternalUrls: options.nativefier.blockExternalUrls,
|
||||
bounce: options.nativefier.bounce,
|
||||
browserwindowOptions: options.nativefier.browserwindowOptions,
|
||||
buildDate: new Date().getTime(),
|
||||
buildVersion: options.packager.buildVersion,
|
||||
clearCache: options.nativefier.clearCache,
|
||||
counter: options.nativefier.counter,
|
||||
crashReporter: options.nativefier.crashReporter,
|
||||
darwinDarkModeSupport: options.packager.darwinDarkModeSupport,
|
||||
derefSymlinks: options.packager.derefSymlinks,
|
||||
disableContextMenu: options.nativefier.disableContextMenu,
|
||||
disableDevTools: options.nativefier.disableDevTools,
|
||||
disableGpu: options.nativefier.disableGpu,
|
||||
disableOldBuildWarning: options.nativefier.disableOldBuildWarning,
|
||||
diskCacheSize: options.nativefier.diskCacheSize,
|
||||
download: options.packager.download,
|
||||
electronVersionUsed: options.packager.electronVersion,
|
||||
enableEs3Apis: options.nativefier.enableEs3Apis,
|
||||
executableName: options.packager.executableName,
|
||||
fastQuit: options.nativefier.fastQuit,
|
||||
fileDownloadOptions: options.nativefier.fileDownloadOptions,
|
||||
flashPluginDir: options.nativefier.flashPluginDir,
|
||||
fullScreen: options.nativefier.fullScreen,
|
||||
globalShortcuts: options.nativefier.globalShortcuts,
|
||||
height: options.nativefier.height,
|
||||
helperBundleId: options.packager.helperBundleId,
|
||||
hideWindowFrame: options.nativefier.hideWindowFrame,
|
||||
ignoreCertificate: options.nativefier.ignoreCertificate,
|
||||
ignoreGpuBlacklist: options.nativefier.ignoreGpuBlacklist,
|
||||
insecure: options.nativefier.insecure,
|
||||
internalUrls: options.nativefier.internalUrls,
|
||||
blockExternalUrls: options.nativefier.blockExternalUrls,
|
||||
maxHeight: options.nativefier.maxHeight,
|
||||
isUpgrade: options.packager.upgrade,
|
||||
junk: options.packager.junk,
|
||||
maximize: options.nativefier.maximize,
|
||||
maxHeight: options.nativefier.maxHeight,
|
||||
maxWidth: options.nativefier.maxWidth,
|
||||
minHeight: options.nativefier.minHeight,
|
||||
minWidth: options.nativefier.minWidth,
|
||||
name: options.packager.name,
|
||||
nativefierVersion: options.nativefier.nativefierVersion,
|
||||
osxNotarize: options.packager.osxNotarize,
|
||||
osxSign: options.packager.osxSign,
|
||||
processEnvs: options.nativefier.processEnvs,
|
||||
protocols: options.packager.protocols,
|
||||
proxyRules: options.nativefier.proxyRules,
|
||||
prune: options.packager.prune,
|
||||
quiet: options.packager.quiet,
|
||||
showMenuBar: options.nativefier.showMenuBar,
|
||||
singleInstance: options.nativefier.singleInstance,
|
||||
targetUrl: options.packager.targetUrl,
|
||||
titleBarStyle: options.nativefier.titleBarStyle,
|
||||
tray: options.nativefier.tray,
|
||||
usageDescription: options.packager.usageDescription,
|
||||
userAgent: options.nativefier.userAgent,
|
||||
userAgentOverriden: options.nativefier.userAgentOverriden,
|
||||
versionString: options.nativefier.versionString,
|
||||
width: options.nativefier.width,
|
||||
win32metadata: options.packager.win32metadata,
|
||||
disableOldBuildWarning: options.nativefier.disableOldBuildWarning,
|
||||
x: options.nativefier.x,
|
||||
y: options.nativefier.y,
|
||||
zoom: options.nativefier.zoom,
|
||||
buildDate: new Date().getTime(),
|
||||
// OLD_BUILD_WARNING_TEXT is an undocumented env. var to let *packagers*
|
||||
// tweak the message shown on warning about an old build, to something
|
||||
// more tailored to their audience (who might not even know Nativefier).
|
||||
|
20
src/cli.ts
20
src/cli.ts
@ -9,6 +9,7 @@ import * as log from 'loglevel';
|
||||
import { isArgFormatInvalid } from './helpers/helpers';
|
||||
import { supportedArchs, supportedPlatforms } from './infer/inferOs';
|
||||
import { buildNativefierApp } from './main';
|
||||
import { NativefierOptions } from './options/model';
|
||||
import { parseBooleanOrString, parseJson } from './utils/parseUtils';
|
||||
|
||||
// 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
|
||||
.name('nativefier')
|
||||
.version(packageJson.version, '-v, --version')
|
||||
.arguments('<targetUrl> [dest]')
|
||||
.arguments('[targetUrl] [dest]')
|
||||
.action((url, outputDirectory) => {
|
||||
positionalOptions.targetUrl = url;
|
||||
positionalOptions.out = outputDirectory;
|
||||
})
|
||||
.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(
|
||||
new commander.Option('-p, --platform <value>').choices(
|
||||
supportedPlatforms,
|
||||
@ -291,7 +296,18 @@ if (require.main === module) {
|
||||
commander.help();
|
||||
}
|
||||
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) => {
|
||||
log.error('Error during build. Run with --verbose for details.', error);
|
||||
});
|
||||
|
19
src/helpers/fsHelpers.ts
Normal file
19
src/helpers/fsHelpers.ts
Normal 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;
|
||||
}
|
||||
}
|
186
src/helpers/upgrade/executableHelpers.ts
Normal file
186
src/helpers/upgrade/executableHelpers.ts
Normal 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;
|
||||
}
|
39
src/helpers/upgrade/plistInfoXMLHelpers.ts
Normal file
39
src/helpers/upgrade/plistInfoXMLHelpers.ts
Normal 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)
|
||||
}
|
42
src/helpers/upgrade/rceditGet.ts
Normal file
42
src/helpers/upgrade/rceditGet.ts
Normal 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;
|
||||
}
|
||||
}
|
199
src/helpers/upgrade/upgrade.ts
Normal file
199
src/helpers/upgrade/upgrade.ts
Normal 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;
|
||||
}
|
@ -1,10 +1,24 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { DEFAULT_ELECTRON_VERSION } from './constants';
|
||||
import { getTempDir } from './helpers/helpers';
|
||||
import { inferArch } from './infer/inferOs';
|
||||
import { inferUserAgent } from './infer/inferUserAgent';
|
||||
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;
|
||||
|
||||
switch (inputOptions.platform) {
|
||||
@ -18,7 +32,9 @@ function checkApp(appRoot: string, inputOptions: any): void {
|
||||
relativeAppFolder = 'resources/app';
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown app platform');
|
||||
throw new Error(
|
||||
`Unknown app platform: ${new String(inputOptions.platform).toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const appPath = path.join(appRoot, relativeAppFolder);
|
||||
@ -36,6 +52,28 @@ function checkApp(appRoot: string, inputOptions: any): void {
|
||||
const iconPath = path.join(appPath, iconFile);
|
||||
expect(fs.existsSync(iconPath)).toBe(true);
|
||||
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', () => {
|
||||
@ -52,7 +90,54 @@ describe('Nativefier', () => {
|
||||
platform,
|
||||
};
|
||||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -3,6 +3,8 @@ import * as electronPackager from 'electron-packager';
|
||||
export interface ElectronPackagerOptions extends electronPackager.Options {
|
||||
targetUrl: string;
|
||||
platform: string;
|
||||
upgrade: boolean;
|
||||
upgradeFrom?: string;
|
||||
}
|
||||
|
||||
export interface AppOptions {
|
||||
@ -24,6 +26,7 @@ export interface AppOptions {
|
||||
disableGpu: boolean;
|
||||
disableOldBuildWarning: boolean;
|
||||
diskCacheSize: number;
|
||||
electronVersionUsed?: string;
|
||||
enableEs3Apis: boolean;
|
||||
fastQuit: boolean;
|
||||
fileDownloadOptions: any;
|
||||
@ -46,6 +49,7 @@ export interface AppOptions {
|
||||
titleBarStyle: string;
|
||||
tray: string | boolean;
|
||||
userAgent: string;
|
||||
userAgentOverriden: boolean;
|
||||
verbose: boolean;
|
||||
versionString: string;
|
||||
width: number;
|
||||
|
@ -28,7 +28,7 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
|
||||
appCopyright: rawOptions.appCopyright,
|
||||
appVersion: rawOptions.appVersion,
|
||||
arch: rawOptions.arch || inferArch(),
|
||||
asar: rawOptions.conceal || false,
|
||||
asar: rawOptions.asar || rawOptions.conceal || false,
|
||||
buildVersion: rawOptions.buildVersion,
|
||||
darwinDarkModeSupport: rawOptions.darwinDarkModeSupport || false,
|
||||
dir: PLACEHOLDER_APP_DIR,
|
||||
@ -38,8 +38,13 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
|
||||
out: rawOptions.out || process.cwd(),
|
||||
overwrite: rawOptions.overwrite,
|
||||
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
|
||||
upgrade: rawOptions.upgrade !== undefined ? true : false,
|
||||
upgradeFrom: rawOptions.upgrade,
|
||||
win32metadata: rawOptions.win32metadata || {
|
||||
ProductName: rawOptions.name,
|
||||
InternalName: rawOptions.name,
|
||||
@ -86,6 +91,8 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
|
||||
titleBarStyle: rawOptions.titleBarStyle || null,
|
||||
tray: rawOptions.tray || false,
|
||||
userAgent: rawOptions.userAgent,
|
||||
userAgentOverriden:
|
||||
rawOptions.userAgent !== undefined && rawOptions.userAgent !== null,
|
||||
verbose: rawOptions.verbose,
|
||||
versionString: rawOptions.versionString,
|
||||
width: rawOptions.width || 1280,
|
||||
|
Loading…
Reference in New Issue
Block a user