Files
flipper/desktop/scripts/build-release.ts
Michel Weststrate dcfeb4a4d5 Clean up packages and types
Summary:
This diff removes most deps from the root package.json, which now only contains electron and shared build / test infra structure: lint, prettier, jest, typescript.

This makes it possible to control much better which packages are used where, as all sub packages now have their deps explicitly in their package.json instead of incidentally shared. This allows for example to disable DOM types for all packages by default (flipper-plugin, ui(-core) and app still request it), and in the next diff I hope to add to this that nodeJS types are no longer shared either, so that UI oriented packages will generate compile errors when using Node built-ins

This diff removes most deps that were currently unused, and dedupes a bunch of other ones, so the build should probably be a bit smaller now as well:

{F686704253}

{F686704295}

Reviewed By: antonk52

Differential Revision: D33062859

fbshipit-source-id: 5afaa4f2103d055188382a3370c1fffa295a298a
2021-12-16 14:54:59 -08:00

413 lines
13 KiB
TypeScript
Executable File

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import path from 'path';
import fs from 'fs-extra';
import {
Platform,
Arch,
ElectronDownloadOptions,
build,
AfterPackContext,
AppInfo,
} from 'electron-builder';
import {spawn} from 'promisify-child-process';
import {
buildFolder,
compileRenderer,
compileMain,
die,
getVersionNumber,
genMercurialRevision,
prepareDefaultPlugins,
moveSourceMaps,
} from './build-utils';
import fetch from '@adobe/node-fetch-retry';
import isFB from './isFB';
import copyPackageWithDependencies from './copy-package-with-dependencies';
import {staticDir, distDir} from './paths';
import yargs from 'yargs';
import {WinPackager} from 'app-builder-lib/out/winPackager';
// eslint-disable-next-line node/no-extraneous-import
import type {Icon} from 'flipper-ui-core';
// Used in some places to avoid release-to-release changes. Needs
// to be this high for some MacOS-specific things that I can't
// remember right now.
const FIX_RELEASE_VERSION = '50.0.0';
const argv = yargs
.usage('yarn build [args]')
.version(false)
.options({
mac: {
type: 'boolean',
group: 'targets',
},
'mac-dmg': {
type: 'boolean',
group: 'targets',
},
win: {
type: 'boolean',
group: 'targets',
},
linux: {
type: 'boolean',
group: 'targets',
},
'linux-deb': {
type: 'boolean',
group: 'targets',
},
'linux-snap': {
type: 'boolean',
group: 'targets',
},
version: {
description:
'Unique build identifier to be used as the version patch part for the build',
type: 'number',
},
channel: {
description: 'Release channel for the build',
choices: ['stable', 'insiders'],
default: 'stable',
},
'bundled-plugins': {
describe:
'Enables bundling of plugins into Flipper bundle. Env var FLIPPER_NO_BUNDLED_PLUGINS is equivalent to the command-line option "--no-bundled-plugins".',
type: 'boolean',
},
'rebuild-plugins': {
describe:
'Enables rebuilding of default plugins on Flipper build. Only make sense in conjunction with "--no-bundled-plugins". Enabled by default, but if disabled using "--no-plugin-rebuild", then plugins are just released as is without rebuilding. This can save some time if you know plugin bundles are already up-to-date.',
type: 'boolean',
},
'default-plugins-dir': {
describe:
'Directory with prepared list of default plugins which will be included into the Flipper distribution as "defaultPlugins" dir',
type: 'string',
},
'source-map-dir': {
describe:
'Directory to write the main.bundle.map and bundle.map files for the main and render bundle sourcemaps, respectively',
type: 'string',
},
})
.help()
.strict()
.check((argv) => {
const targetSpecified =
argv.mac ||
argv['mac-dmg'] ||
argv.win ||
argv.linux ||
argv['linux-deb'] ||
argv['linux-snap'];
if (!targetSpecified) {
throw new Error('No targets specified. eg. --mac, --win, or --linux');
}
return true;
})
.parse(process.argv.slice(1));
if (isFB) {
process.env.FLIPPER_FB = 'true';
}
process.env.FLIPPER_RELEASE_CHANNEL = argv.channel;
if (argv['bundled-plugins'] === false) {
process.env.FLIPPER_NO_BUNDLED_PLUGINS = 'true';
} else if (argv['bundled-plugins'] === true) {
delete process.env.FLIPPER_NO_BUNDLED_PLUGINS;
}
if (argv['rebuild-plugins'] === false) {
process.env.FLIPPER_NO_REBUILD_PLUGINS = 'true';
} else if (argv['rebuild-plugins'] === true) {
delete process.env.FLIPPER_NO_REBUILD_PLUGINS;
}
if (argv['default-plugins-dir']) {
process.env.FLIPPER_DEFAULT_PLUGINS_DIR = argv['default-plugins-dir'];
}
async function generateManifest(versionNumber: string) {
await fs.writeFile(
path.join(distDir, 'manifest.json'),
JSON.stringify({
package: 'com.facebook.sonar',
version_name: versionNumber,
}),
);
}
async function modifyPackageManifest(
buildFolder: string,
versionNumber: string,
hgRevision: string | null,
channel: string,
) {
// eslint-disable-next-line no-console
console.log('Creating package.json manifest');
// eslint-disable-next-line flipper/no-relative-imports-across-packages
const manifest = require('../package.json');
// eslint-disable-next-line flipper/no-relative-imports-across-packages
const manifestStatic = require('../static/package.json');
// The manifest's dependencies are bundled with the final app by
// electron-builder. We want to bundle the dependencies from the static-folder
// because all dependencies from the root-folder are already bundled by metro.
manifest.dependencies = manifestStatic.dependencies;
manifest.main = 'index.js';
manifest.version = versionNumber;
if (hgRevision != null) {
manifest.revision = hgRevision;
}
manifest.releaseChannel = channel;
await fs.writeFile(
path.join(buildFolder, 'package.json'),
JSON.stringify(manifest, null, ' '),
);
}
// Same as for MacOS, we are hardcoding version information and other
// properties on Windows that change from release to release to improve cache
// behaviour. This is especially important as the .exe contains the Electron/Chromium
// frameworks which are > 120 MB in size.
// Note: This is run *after* packing has completed, meaning that ZIP file will
// not include these changes. As our packer operates on the unpacked results,
// this doesn't matter.
async function afterPack(context: AfterPackContext) {
if (context.electronPlatformName !== 'win32' || !isFB) {
return;
}
// Because all of this is implemented in an OOP way,
// we're having to do a lot of hacky shit here to
// temporarily override properties. While it may look
// cleaner to just have a big ts-ignore block, by
// only disabling `readonly` flags, we at least
// get remaining guarantees regarding type alignment
// and property names being present.
type Mutable<T> = {-readonly [P in keyof T]: T[P]};
const originalPackager = Object.assign({}, context.packager);
const packager = context.packager as unknown as WinPackager;
const appInfo: Mutable<AppInfo> = packager.appInfo;
const exeFileName = `${packager.appInfo.productFilename}.exe`;
appInfo.version = FIX_RELEASE_VERSION;
appInfo.buildVersion = FIX_RELEASE_VERSION;
appInfo.shortVersion = FIX_RELEASE_VERSION;
// Contains a side-effect dependent on the current year.
Object.defineProperty(appInfo, 'copyright', {
get: () => 'Facebook, Inc.',
});
packager.signAndEditResources(
path.join(context.appOutDir, exeFileName),
context.arch,
context.outDir,
path.basename(exeFileName, '.exe'),
packager.platformSpecificBuildOptions.requestedExecutionLevel,
);
(context as Mutable<AfterPackContext>).packager = originalPackager;
}
async function buildDist(buildFolder: string) {
const targetsRaw: Map<Platform, Map<Arch, string[]>>[] = [];
const postBuildCallbacks: (() => void)[] = [];
if (argv.mac || argv['mac-dmg']) {
targetsRaw.push(Platform.MAC.createTarget(['dir']));
// You can build mac apps on Linux but can't build dmgs, so we separate those.
if (argv['mac-dmg']) {
targetsRaw.push(Platform.MAC.createTarget(['dmg']));
}
postBuildCallbacks.push(() =>
spawn('zip', ['-qyr9', '../Flipper-mac.zip', 'Flipper.app'], {
cwd: path.join(distDir, 'mac'),
encoding: 'utf-8',
}),
);
}
if (argv.linux || argv['linux-deb'] || argv['linux-snap']) {
targetsRaw.push(Platform.LINUX.createTarget(['zip']));
if (argv['linux-deb']) {
// linux targets can be:
// AppImage, snap, deb, rpm, freebsd, pacman, p5p, apk, 7z, zip, tar.xz, tar.lz, tar.gz, tar.bz2, dir
targetsRaw.push(Platform.LINUX.createTarget(['deb']));
}
if (argv['linux-snap']) {
targetsRaw.push(Platform.LINUX.createTarget(['snap']));
}
}
if (argv.win) {
targetsRaw.push(Platform.WINDOWS.createTarget(['zip']));
}
if (!targetsRaw.length) {
throw new Error('No targets specified. eg. --mac, --win, or --linux');
}
// merge all target maps into a single map
let targetsMerged: [Platform, Map<Arch, string[]>][] = [];
for (const target of targetsRaw) {
targetsMerged = targetsMerged.concat(Array.from(target));
}
const targets = new Map(targetsMerged);
const electronDownloadOptions: ElectronDownloadOptions = {};
if (process.env.electron_config_cache) {
electronDownloadOptions.cache = process.env.electron_config_cache;
}
try {
await build({
publish: 'never',
config: {
appId: `com.facebook.sonar`,
productName: 'Flipper',
directories: {
buildResources: buildFolder,
output: distDir,
},
electronDownload: electronDownloadOptions,
npmRebuild: false,
linux: {
executableName: 'flipper',
},
mac: {
bundleVersion: FIX_RELEASE_VERSION,
},
win: {
signAndEditExecutable: !isFB,
},
afterPack,
},
projectDir: buildFolder,
targets,
});
return await Promise.all(postBuildCallbacks.map((p) => p()));
} catch (err) {
return die(err);
}
}
async function copyStaticFolder(buildFolder: string) {
console.log(`⚙️ Copying static package with dependencies...`);
await copyPackageWithDependencies(staticDir, buildFolder);
console.log('✅ Copied static package with dependencies.');
}
// Takes a string like 'star', or 'star-outline', and converts it to
// {trimmedName: 'star', variant: 'filled'} or {trimmedName: 'star', variant: 'outline'}
function getIconPartsFromName(icon: string): {
trimmedName: string;
variant: 'outline' | 'filled';
} {
const isOutlineVersion = icon.endsWith('-outline');
const trimmedName = isOutlineVersion ? icon.replace('-outline', '') : icon;
const variant = isOutlineVersion ? 'outline' : 'filled';
return {trimmedName: trimmedName, variant: variant};
}
async function downloadIcons(buildFolder: string) {
const icons: Icons = JSON.parse(
await fs.promises.readFile(path.join(buildFolder, 'icons.json'), {
encoding: 'utf8',
}),
);
const iconURLs = Object.entries(icons).reduce<Icon[]>(
(acc, [entryName, sizes]) => {
const {trimmedName: name, variant} = getIconPartsFromName(entryName);
acc.push(
// get icons in @1x and @2x
...sizes.map((size) => ({name, variant, size, density: 1})),
...sizes.map((size) => ({name, variant, size, density: 2})),
);
return acc;
},
[],
);
return Promise.all(
iconURLs.map((icon) => {
const url = getPublicIconUrl(icon);
return fetch(url, {
retryOptions: {
// Be default, only 5xx are retried but we're getting the odd 404
// which goes away on a retry for some reason.
retryOnHttpResponse: (res) => res.status >= 400,
},
})
.then((res) => {
if (res.status !== 200) {
throw new Error(
// eslint-disable-next-line prettier/prettier
`Could not download the icon ${name} from ${url}: got status ${res.status}`,
);
}
return res;
})
.then(
(res) =>
new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(
path.join(buildFolder, buildLocalIconPath(icon)),
);
res.body.pipe(fileStream);
res.body.on('error', reject);
fileStream.on('finish', resolve);
}),
);
}),
);
}
(async () => {
const dir = await buildFolder();
// eslint-disable-next-line no-console
console.log('Created build directory', dir);
await compileMain();
await prepareDefaultPlugins(argv.channel === 'insiders');
await copyStaticFolder(dir);
await downloadIcons(dir);
await compileRenderer(dir);
await moveSourceMaps(dir, argv['source-map-dir']);
const versionNumber = getVersionNumber(argv.version);
const hgRevision = await genMercurialRevision();
await modifyPackageManifest(dir, versionNumber, hgRevision, argv.channel);
await fs.ensureDir(distDir);
await generateManifest(versionNumber);
await buildDist(dir);
// eslint-disable-next-line no-console
console.log('✨ Done');
process.exit();
})();
export type Icons = {
[key: string]: Icon['size'][];
};
// should match flipper-ui-core/src/utils/icons.tsx
export function getPublicIconUrl({name, variant, size, density}: Icon) {
return `https://facebook.com/assets/?name=${name}&variant=${variant}&size=${size}&set=facebook_icons&density=${density}x`;
}
// should match app/src/utils/icons.tsx
function buildLocalIconPath(icon: Icon) {
return path.join(
'icons',
`${icon.name}-${icon.variant}-${icon.size}@${icon.density}x.png`,
);
}