Fix upgrade not working (#1286)

* Attempt to get upgrade working right; in progress

* Got it fixed in Mac

* Fix some linting errors

* Finish fixing upgrade + tests

* Integration testing for global shortcuts

* Regenerate shrinkwrap

* Get rid of deprecated rmdirSync

* Remove instead of rm for 12.x support

* Make dereferencing platform dependent

* Fix folder copy funkiness

* Whoops

* Whoops 2: Extra Whoops

* Update Electron to 13.5.1; Fix auth manual tests

* Rework relock

* Add a request for help.

* Update @types/node to 14
This commit is contained in:
Adam Weeden 2021-11-29 12:01:20 -05:00 committed by GitHub
parent adcf7c4c0c
commit b9c5e2b464
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1165 additions and 841 deletions

View File

@ -235,9 +235,9 @@
"dev": true
},
"node_modules/debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
@ -1375,9 +1375,9 @@
"dev": true
},
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"dev": true,
"requires": {
"ms": "2.1.2"

1699
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,7 @@
"lint": "eslint shared app src --ext .ts",
"list-outdated-deps": "npm out; cd app && npm out; true",
"prepare": "cd app && npm ci && cd .. && npm run build",
"relock": "rm -rf ./node_modules/ ./app/node_modules/ ./npm-shrinkwrap.json ./app/npm-shrinkwrap.json; npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out; cd app && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out; cd .. && true",
"relock": "rm -rf ./node_modules/ ./app/node_modules/ ./npm-shrinkwrap.json ./app/npm-shrinkwrap.json && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out && cd app && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out",
"test:integration": "jest --testRegex '.*integration-test.js'",
"test:manual": "npm run build && ./.github/manual-test",
"test:unit": "jest",
@ -55,6 +55,7 @@
"dependencies": {
"axios": "^0.24.0",
"electron-packager": "^15.2.0",
"fs-extra": "^10.0.0",
"gitcloud": "^0.2.3",
"hasbin": "^1.2.3",
"loglevel": "^1.7.1",
@ -67,10 +68,11 @@
},
"devDependencies": {
"@types/debug": "^4.1.6",
"@types/fs-extra": "^9.0.13",
"@types/hasbin": "^1.2.0",
"@types/jest": "^27.0.1",
"@types/ncp": "^2.0.5",
"@types/node": "^12.20.16",
"@types/node": "14.14.20",
"@types/page-icon": "^0.3.4",
"@types/tmp": "^0.2.1",
"@typescript-eslint/eslint-plugin": "^5.3.0",

View File

@ -183,7 +183,7 @@ export type RawOptions = {
singleInstance?: boolean;
targetUrl?: string;
titleBarStyle?: TitleBarValue;
tray: TrayValue;
tray?: TrayValue;
upgrade?: string | boolean;
upgradeFrom?: string;
userAgent?: string;

View File

@ -1,12 +1,13 @@
import * as path from 'path';
import * as electronGet from '@electron/get';
import * as chalk from 'chalk';
import electronPackager from 'electron-packager';
import * as fs from 'fs-extra';
import * as log from 'loglevel';
import { convertIconIfNecessary } from './buildIcon';
import {
copyFileOrDir,
getTempDir,
hasWine,
isWindows,
@ -49,7 +50,7 @@ async function copyIconsIfNecessary(
log.debug('Copying icon for tray application');
const trayIconFileName = `tray-icon.png`;
const destIconPath = path.join(appPath, 'icon.png');
await copyFileOrDir(
await fs.copy(
`${path.dirname(options.packager.icon)}/${trayIconFileName}`,
destIconPath,
);
@ -64,7 +65,7 @@ async function copyIconsIfNecessary(
const destIconPath = path.join(appPath, destFileName);
log.debug(`Copying icon ${options.packager.icon} to`, destIconPath);
await copyFileOrDir(options.packager.icon, destIconPath);
await fs.copy(options.packager.icon, destIconPath);
}
/**
@ -129,12 +130,22 @@ function trimUnprocessableOptions(options: AppOptions): void {
export async function buildNativefierApp(
rawOptions: RawOptions,
): Promise<string | undefined> {
log.warn(
new chalk.Instance().yellowBright.bold(
'\n\n Hi! Nativefier is minimally maintained these days, and needs more hands.\n' +
' If you have the time & motivation, help with bugfixes and maintenance is VERY welcome.\n' +
' Please go to https://github.com/nativefier/nativefier and help how you can. Thanks.\n\n',
),
);
log.info('\nProcessing options...');
let finalOutDirectory = rawOptions.out ?? process.cwd();
if (isUpgrade(rawOptions)) {
log.debug('Attempting to upgrade from', rawOptions.upgradeFrom);
const oldApp = findUpgradeApp(rawOptions.upgradeFrom as string);
if (oldApp === null) {
if (!oldApp) {
throw new Error(
`Could not find an old Nativfier app in "${
rawOptions.upgradeFrom as string
@ -143,7 +154,8 @@ export async function buildNativefierApp(
}
rawOptions = useOldAppOptions(rawOptions, oldApp);
if (rawOptions.out === undefined && rawOptions.overwrite) {
rawOptions.out = path.dirname(rawOptions.upgradeFrom as string);
finalOutDirectory = oldApp.appRoot;
rawOptions.out = getTempDir('appUpgrade', 0o755);
}
}
log.debug('rawOptions', rawOptions);
@ -172,7 +184,7 @@ export async function buildNativefierApp(
await prepareElectronApp(options.packager.dir, tmpPath, options);
log.info('\nConverting icons...');
options.packager.dir = tmpPath; // const optionsWithTmpPath = { ...options, dir: tmpPath };
options.packager.dir = tmpPath;
convertIconIfNecessary(options);
await copyIconsIfNecessary(options, tmpPath);
@ -184,20 +196,64 @@ export async function buildNativefierApp(
const appPathArray = await electronPackager(options.packager);
log.info('\nFinalizing build...');
const appPath = getAppPath(appPathArray);
let appPath = getAppPath(appPathArray);
if (appPath) {
let osRunHelp = '';
if (options.packager.platform === 'win32') {
osRunHelp = `the contained .exe file.`;
} else if (options.packager.platform === 'linux') {
osRunHelp = `the contained executable file (prefixing with ./ if necessary)\nMenu/desktop shortcuts are up to you, because Nativefier cannot know where you're going to move the app. Search for "linux .desktop file" for help, or see https://wiki.archlinux.org/index.php/Desktop_entries`;
} else if (options.packager.platform === 'darwin') {
osRunHelp = `the app bundle.`;
}
log.info(
`App built to ${appPath}, move to wherever it makes sense for you and run ${osRunHelp}`,
);
if (!appPath) {
throw new Error('App Path could not be determined.');
}
if (
options.packager.upgrade &&
options.packager.upgradeFrom &&
options.packager.overwrite
) {
if (options.packager.platform === 'darwin') {
try {
// This is needed due to a funky thing that happens when copying Squirrel.framework
// over where it gets into a circular file reference somehow.
await fs.remove(
path.join(
finalOutDirectory,
`${options.packager.name ?? ''}.app`,
'Contents',
'Frameworks',
),
);
} catch (err: unknown) {
log.warn(
'Encountered an error when attempting to pre-delete old frameworks:',
err,
);
}
await fs.copy(
path.join(appPath, `${options.packager.name ?? ''}.app`),
path.join(finalOutDirectory, `${options.packager.name ?? ''}.app`),
{
overwrite: options.packager.overwrite,
preserveTimestamps: true,
},
);
} else {
await fs.copy(appPath, finalOutDirectory, {
overwrite: options.packager.overwrite,
preserveTimestamps: true,
});
}
await fs.remove(appPath);
appPath = finalOutDirectory;
}
let osRunHelp = '';
if (options.packager.platform === 'win32') {
osRunHelp = `the contained .exe file.`;
} else if (options.packager.platform === 'linux') {
osRunHelp = `the contained executable file (prefixing with ./ if necessary)\nMenu/desktop shortcuts are up to you, because Nativefier cannot know where you're going to move the app. Search for "linux .desktop file" for help, or see https://wiki.archlinux.org/index.php/Desktop_entries`;
} else if (options.packager.platform === 'darwin') {
osRunHelp = `the app bundle.`;
}
log.info(
`App built to ${appPath}, move to wherever it makes sense for you and run ${osRunHelp}`,
);
return appPath;
}

View File

@ -1,11 +1,10 @@
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as fs from 'fs-extra';
import * as path from 'path';
import { promisify } from 'util';
import * as log from 'loglevel';
import { copyFileOrDir, generateRandomSuffix } from '../helpers/helpers';
import { generateRandomSuffix } from '../helpers/helpers';
import {
AppOptions,
OutputOptions,
@ -14,8 +13,6 @@ import {
import { parseJson } from '../utils/parseUtils';
import { DEFAULT_APP_NAME } from '../constants';
const writeFileAsync = promisify(fs.writeFile);
/**
* Only picks certain app args to pass to nativefier.json
*/
@ -133,7 +130,7 @@ async function maybeCopyScripts(
const destFileName = `inject-${postFixHash}${path.extname(src)}`;
const destPath = path.join(dest, 'inject', destFileName);
log.debug(`Copying injection file "${src}" to "${destPath}"`);
await copyFileOrDir(src, destPath);
await fs.copy(src, destPath);
}
}
@ -186,7 +183,7 @@ export async function prepareElectronApp(
): Promise<void> {
log.debug(`Copying electron app from ${src} to ${dest}`);
try {
await copyFileOrDir(src, dest);
await fs.copy(src, dest);
} catch (err: unknown) {
throw `Error copying electron app from ${src} to temp dir ${dest}. Error: ${
(err as Error).message
@ -194,16 +191,14 @@ export async function prepareElectronApp(
}
const appJsonPath = path.join(dest, '/nativefier.json');
log.debug(`Writing app config to ${appJsonPath}`);
await writeFileAsync(
appJsonPath,
JSON.stringify(pickElectronAppArgs(options), null, 2),
);
const pickedOptions = pickElectronAppArgs(options);
log.debug(`Writing app config to ${appJsonPath}`, pickedOptions);
await fs.writeFile(appJsonPath, JSON.stringify(pickedOptions));
if (options.nativefier.bookmarksMenu) {
const bookmarksJsonPath = path.join(dest, '/bookmarks.json');
try {
await copyFileOrDir(options.nativefier.bookmarksMenu, bookmarksJsonPath);
await fs.copy(options.nativefier.bookmarksMenu, bookmarksJsonPath);
} catch (err: unknown) {
log.error('Error copying bookmarks menu config file.', err);
}

View File

@ -638,6 +638,26 @@ if (require.main === module) {
...parsedArgs,
};
if (options.verbose) {
log.setLevel('trace');
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
require('debug').enable('electron-packager');
} catch (err: unknown) {
log.debug(
'Failed to enable electron-packager debug output. This should not happen,',
'and suggests their internals changed. Please report an issue.',
);
}
log.debug(
'Running in verbose mode! This will produce a mountain of logs and',
'is recommended only for troubleshooting or if you like Shakespeare.',
);
} else {
log.setLevel('info');
}
checkInternet();
buildNativefierApp(options).catch((error) => {

View File

@ -7,7 +7,6 @@ import axios from 'axios';
import * as dns from 'dns';
import * as hasbin from 'hasbin';
import * as log from 'loglevel';
import { ncp } from 'ncp';
import * as tmp from 'tmp';
import { parseJson } from '../utils/parseUtils';
@ -58,20 +57,6 @@ export function getTempDir(prefix: string, mode?: number): string {
}).name;
}
export async function copyFileOrDir(
sourceFileOrDir: string,
dest: string,
): Promise<void> {
return new Promise((resolve, reject) => {
ncp(sourceFileOrDir, dest, (error: Error[] | null): void => {
if (error) {
reject(error);
}
resolve();
});
});
}
export function downloadFile(
fileUrl: string,
): Promise<DownloadResult | undefined> {

View File

@ -36,7 +36,6 @@ function getExecutableArch(
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';
@ -45,7 +44,7 @@ function getExecutableArch(
default:
return undefined;
}
case 'windows':
case 'win32':
// https://en.wikibooks.org/wiki/X86_Disassembly/Windows_Executable_Files#COFF_Header
switch ((exeBytes[0x7d] << 8) + exeBytes[0x7c]) {
case 0x014c:
@ -118,7 +117,7 @@ export function getOptionsFromExecutable(
log.debug('This looks like a Windows app...');
if (newOptions.platform === undefined) {
newOptions.platform = 'windows';
newOptions.platform = 'win32';
}
executablePath = path.join(
appRoot,

View File

@ -14,6 +14,7 @@ import { parseJson } from '../../utils/parseUtils';
export type UpgradeAppInfo = {
appResourcesDir: string;
appRoot: string;
options: NativefierOptions;
};
@ -40,6 +41,25 @@ function findUpgradeAppResourcesDir(searchDir: string): string | null {
return null;
}
function getAppRoot(
appResourcesDir: string,
options: NativefierOptions,
): string {
switch (options.platform) {
case 'darwin':
return path.resolve(path.join(appResourcesDir, '..', '..', '..', '..'));
case 'linux':
case 'win32':
return path.resolve(path.join(appResourcesDir, '..', '..'));
default:
throw new Error(
`Could not find the app root for platform: ${
options.platform ?? 'undefined'
}`,
);
}
}
function getIconPath(appResourcesDir: string): string | undefined {
const icnsPath = path.join(appResourcesDir, '..', 'electron.icns');
if (fileExists(icnsPath)) {
@ -163,7 +183,7 @@ export function findUpgradeApp(upgradeFrom: string): UpgradeAppInfo | null {
const nativefierJSONPath = path.join(appResourcesDir, 'nativefier.json');
log.debug(`Loading ${nativefierJSONPath}`);
const options = parseJson<NativefierOptions>(
let options = parseJson<NativefierOptions>(
fs.readFileSync(nativefierJSONPath, 'utf8'),
);
@ -175,11 +195,18 @@ export function findUpgradeApp(upgradeFrom: string): UpgradeAppInfo | null {
options.electronVersion = undefined;
options = {
...options,
...getOptionsFromExecutable(appResourcesDir, options),
};
const appRoot = getAppRoot(appResourcesDir, options);
return {
appResourcesDir,
appRoot,
options: {
...options,
...getOptionsFromExecutable(appResourcesDir, options),
...getInfoPListOptions(appResourcesDir, options),
asar: options.asar !== undefined ? options.asar : isAsar(appResourcesDir),
icon: getIconPath(appResourcesDir),
@ -197,7 +224,6 @@ export function useOldAppOptions(
rawOptions.out = rawOptions.targetUrl;
}
log.debug('rawOptions', rawOptions);
log.debug('oldApp', oldApp);
const combinedOptions = { ...rawOptions, ...oldApp.options };

View File

@ -14,6 +14,7 @@ export const supportedPlatforms = [
'mac',
'mas',
'osx',
'win32',
'windows',
];

View File

@ -10,7 +10,11 @@ import { getLatestSafariVersion } from './infer/browsers/inferSafariVersion';
import { inferArch } from './infer/inferOs';
import { buildNativefierApp } from './main';
import { userAgent } from './options/fields/userAgent';
import { NativefierOptions, RawOptions } from '../shared/src/options/model';
import {
GlobalShortcut,
NativefierOptions,
RawOptions,
} from '../shared/src/options/model';
import { parseJson } from './utils/parseUtils';
async function checkApp(
@ -27,25 +31,13 @@ async function checkApp(
).toBe(appRoot);
}
let relativeAppFolder: string;
let relativeResourcesDir = 'resources';
switch (inputOptions.platform) {
case 'darwin':
relativeAppFolder = path.join('Google.app', 'Contents/Resources/app');
break;
case 'linux':
relativeAppFolder = 'resources/app';
break;
case 'win32':
relativeAppFolder = 'resources/app';
break;
default:
throw new Error(
`Unknown app platform: ${new String(inputOptions.platform).toString()}`,
);
if (inputOptions.platform === 'darwin') {
relativeResourcesDir = path.join('Google.app', 'Contents', 'Resources');
}
const appPath = path.join(appRoot, relativeAppFolder);
const appPath = path.join(appRoot, relativeResourcesDir, 'app');
const configPath = path.join(appPath, 'nativefier.json');
const nativefierConfig: NativefierOptions | undefined =
@ -59,7 +51,11 @@ async function checkApp(
// Test icon writing
const iconFile =
inputOptions.platform === 'darwin' ? '../electron.icns' : 'icon.png';
inputOptions.platform === 'darwin'
? path.join('..', 'electron.icns')
: inputOptions.platform === 'linux'
? 'icon.png'
: 'icon.ico';
const iconPath = path.join(appPath, iconFile);
expect(fs.existsSync(iconPath)).toEqual(true);
expect(fs.statSync(iconPath).size).toBeGreaterThan(1000);
@ -93,6 +89,21 @@ async function checkApp(
// Test lang
expect(nativefierConfig?.lang).toEqual(inputOptions.lang);
// Test global shortcuts
if (inputOptions.globalShortcuts) {
let shortcutData: GlobalShortcut[] | undefined = [];
if (typeof inputOptions.globalShortcuts === 'string') {
shortcutData = parseJson<GlobalShortcut[]>(
fs.readFileSync(inputOptions.globalShortcuts, 'utf8'),
);
} else {
shortcutData = inputOptions.globalShortcuts;
}
expect(nativefierConfig?.globalShortcuts).toStrictEqual(shortcutData);
}
}
describe('Nativefier', () => {
@ -108,7 +119,6 @@ describe('Nativefier', () => {
overwrite: true,
platform,
targetUrl: 'https://google.com/',
tray: 'false',
};
const appPath = await buildNativefierApp(options);
expect(appPath).not.toBeUndefined();
@ -117,6 +127,34 @@ describe('Nativefier', () => {
);
});
function generateShortcutsFile(dir: string): string {
const shortcuts = [
{
key: 'MediaPlayPause',
inputEvents: [
{
type: 'keyDown',
keyCode: 'Space',
},
],
},
{
key: 'MediaNextTrack',
inputEvents: [
{
type: 'keyDown',
keyCode: 'Right',
},
],
},
];
const filename = path.join(dir, 'shortcuts.json');
fs.writeFileSync(filename, JSON.stringify(shortcuts));
return filename;
}
describe('Nativefier upgrade', () => {
jest.setTimeout(300000);
@ -135,12 +173,13 @@ describe('Nativefier upgrade', () => {
'can upgrade a Nativefier app for platform/arch: %s',
async (baseAppOptions) => {
const tempDirectory = getTempDir('integtestUpgrade1');
const shortcuts = generateShortcutsFile(tempDirectory);
const options: RawOptions = {
electronVersion: '11.2.3',
globalShortcuts: shortcuts,
out: tempDirectory,
overwrite: true,
targetUrl: 'https://google.com/',
tray: 'false',
...baseAppOptions,
};
const appPath = await buildNativefierApp(options);
@ -148,9 +187,8 @@ describe('Nativefier upgrade', () => {
await checkApp(appPath as string, options);
const upgradeOptions: RawOptions = {
upgrade: appPath,
upgrade: appPath as string,
overwrite: true,
tray: 'false',
};
const upgradeAppPath = await buildNativefierApp(upgradeOptions);

View File

@ -226,11 +226,18 @@ export async function getOptions(rawOptions: RawOptions): Promise<AppOptions> {
}
if (rawOptions.globalShortcuts) {
log.debug('Using global shortcuts file at', rawOptions.globalShortcuts);
const globalShortcuts = parseJson<GlobalShortcut[]>(
fs.readFileSync(rawOptions.globalShortcuts as string).toString(),
);
options.nativefier.globalShortcuts = globalShortcuts;
if (typeof rawOptions.globalShortcuts === 'string') {
// This is a file we got over the command line
log.debug('Using global shortcuts file at', rawOptions.globalShortcuts);
const globalShortcuts = parseJson<GlobalShortcut[]>(
fs.readFileSync(rawOptions.globalShortcuts).toString(),
);
options.nativefier.globalShortcuts = globalShortcuts;
} else {
// This is an object we got from an existing config in an upgrade
log.debug('Using global shortcuts object', rawOptions.globalShortcuts);
options.nativefier.globalShortcuts = rawOptions.globalShortcuts;
}
}
await asyncConfig(options);