tutanota/buildSrc/DesktopBuilder.js

234 lines
8.2 KiB
JavaScript
Raw Normal View History

2022-01-07 16:52:02 +01:00
import {resolveLibs} from "./RollupConfig.js"
2021-05-07 10:28:22 +02:00
import {nativeDepWorkaroundPlugin, pluginNativeLoader} from "./RollupPlugins.js"
2019-09-13 13:49:11 +02:00
import nodeResolve from "@rollup/plugin-node-resolve"
import fs from "fs"
import path, {dirname} from "path"
2019-09-13 13:49:11 +02:00
import {rollup} from "rollup"
import {terser} from "rollup-plugin-terser"
import commonjs from "@rollup/plugin-commonjs"
import electronBuilder from "electron-builder"
import generatePackgeJson from "./electron-package-json-template.js"
import {create as createEnv, preludeEnvPlugin} from "./env.js"
import cp from 'child_process'
import util from 'util'
2022-01-07 16:52:02 +01:00
import typescript from "@rollup/plugin-typescript"
import {sqliteNativeBannerPlugin} from "./cachedSqliteProvider.js"
import {fileURLToPath} from "url"
2018-09-24 11:56:41 +02:00
const exec = util.promisify(cp.exec)
const buildSrc = dirname(fileURLToPath(import.meta.url))
const projectRoot = path.resolve(path.join(buildSrc, ".."))
2019-09-13 13:49:11 +02:00
/**
* @param dirname directory this was called from
* @param version application version that gets built
* @param targets: Set<string> - which desktop targets to build and how to package them
* @param updateUrl where the client should pull its updates from, if any
* @param nameSuffix suffix used to distinguish test-, prod- or snapshot builds on the same machine
* @param notarize for the MacOs notarization feature
* @param outDir where copy the finished artifacts
* @param unpacked output desktop client without packing it into an installer
* @returns {Promise<void>}
*/
export async function buildDesktop(
{
dirname,
version,
targets,
updateUrl,
nameSuffix,
notarize,
outDir,
unpacked,
}
) {
2019-09-13 13:49:11 +02:00
// The idea is that we
// - build desktop code into build/dist/desktop
// - package the whole dist directory into the app
// - move installers out of the dist into build/desktop-whatever
// - cleanup dist directory
// It's messy
const targetString = Array.from(targets).join(" ")
2018-10-02 16:19:03 +02:00
console.log("Building desktop client for v" + version + " (" + targetString + ")...")
2019-09-13 13:49:11 +02:00
const updateSubDir = "desktop" + nameSuffix
const distDir = path.join(dirname, "build", "dist")
outDir = path.join(outDir || path.join(distDir, ".."), updateSubDir)
await fs.promises.mkdir(outDir, {recursive: true})
2019-11-07 16:08:49 +01:00
2019-09-13 13:49:11 +02:00
// We need to get the right build of native dependencies. There's a tool called node-gyp which can build for different architectures
// and downloads everything it needs. Usually dependencies build themselves in post-install script.
// Currently we have keytar which avoids building itself if possible and only build
console.log("Updating electron-builder config...")
2019-09-13 13:49:11 +02:00
const content = generatePackgeJson({
nameSuffix,
version,
updateUrl,
iconPath: path.join(dirname, "/resources/desktop-icons/logo-solo-red.png"),
2019-09-13 13:49:11 +02:00
notarize,
unpacked,
sign: (process.env.DEBUG_SIGN && updateUrl !== "") || !!process.env.JENKINS,
})
2019-09-13 13:49:11 +02:00
console.log("updateUrl is", updateUrl)
await fs.promises.writeFile("./build/dist/package.json", JSON.stringify(content), 'utf-8')
if (targets.has("win32")) await getMapirs(distDir)
await maybeGetKeytar(targets)
2019-09-13 13:49:11 +02:00
// prepare files
try {
await fs.promises.rm(path.join(distDir, "..", updateSubDir), {recursive: true})
2019-09-13 13:49:11 +02:00
} catch (e) {
if (e.code !== 'ENOENT') {
throw e
}
}
2019-09-13 13:49:11 +02:00
console.log("Bundling desktop client")
await rollupDesktop(dirname, path.join(distDir, "desktop"), version)
2019-09-13 13:49:11 +02:00
console.log("Starting installer build...")
if (process.platform.startsWith("darwin")) {
// dmg-license is required by electron to build the mac installer
// We can't put dmg-license as a dependency in package.json because
// it will cause npm install to fail if you do it in linux or windows
// We could install it in mac and then it will be in package-lock.json
// but then we will have to be vigilant that it doesn't get removed ever
await exec("npm install dmg-license")
}
2019-09-13 13:49:11 +02:00
// package for linux, win, mac
await electronBuilder.build({
_: ['build'],
win: targets.has("win32") ? [] : undefined,
mac: targets.has("mac") ? [] : undefined,
linux: targets.has("linux") ? [] : undefined,
publish: 'always',
2019-09-13 13:49:11 +02:00
project: distDir
})
console.log("Move output to ", outDir)
await fs.promises.mkdir(outDir, {recursive: true})
await Promise.all(
fs.readdirSync(path.join(distDir, '/installers'))
2021-04-06 14:43:41 +02:00
.filter((file => file.startsWith(content.name) || file.endsWith('.yml') || file.endsWith("-unpacked")))
.map(file => fs.promises.rename(
2022-01-07 16:52:02 +01:00
path.join(distDir, '/installers/', file),
path.join(outDir, file)
)
)
2019-09-13 13:49:11 +02:00
)
await Promise.all([
fs.promises.rm(path.join(distDir, '/installers/'), {recursive: true}),
fs.promises.rm(path.join(distDir, '/node_modules/'), {recursive: true}),
2019-09-13 13:49:11 +02:00
fs.promises.unlink(path.join(distDir, '/package.json')),
fs.promises.unlink(path.join(distDir, '/package-lock.json'),),
])
}
async function rollupDesktop(dirname, outDir, version) {
2019-09-13 13:49:11 +02:00
const mainBundle = await rollup({
2022-01-07 16:52:02 +01:00
input: path.join(dirname, "src/desktop/DesktopMain.ts"),
preserveEntrySignatures: false,
2019-09-13 13:49:11 +02:00
plugins: [
2022-01-07 16:52:02 +01:00
typescript({
tsconfig: "tsconfig.json",
outDir,
}),
2019-09-13 13:49:11 +02:00
resolveLibs(),
nativeDepWorkaroundPlugin(),
2021-05-03 14:03:52 +02:00
pluginNativeLoader(),
2019-09-13 13:49:11 +02:00
nodeResolve({preferBuiltins: true}),
2021-05-07 10:28:22 +02:00
// requireReturnsDefault: "preferred" is needed in order to correclty generate a wrapper for the native keytar module
2021-05-03 14:03:52 +02:00
commonjs({
exclude: "src/**",
requireReturnsDefault: "preferred",
ignoreDynamicRequires: true,
2021-05-03 14:03:52 +02:00
}),
terser(),
preludeEnvPlugin(createEnv({staticUrl: null, version, mode: "Desktop", dist: true})),
sqliteNativeBannerPlugin(
{
environment: "electron",
rootDir: projectRoot,
dstPath: "./build/dist/desktop/better_sqlite3.node"
}
),
2019-09-13 13:49:11 +02:00
]
})
await mainBundle.write({sourcemap: true, format: "commonjs", dir: outDir})
await fs.promises.copyFile(path.join(dirname, "src/desktop/preload.js"), path.join(outDir, "preload.js"))
}
/**
* we can't cross-compile keytar, so we need to have the prebuilt version
* when building a desktop client for windows on linux
*
* napiVersion is the N-API version that's used by keytar.
* the current release artifacts on github are namend accordingly,
* e.g. keytar-v7.7.0-napi-v3-linux-x64.tar.gz for N-API v3
*
* @param targets: Set<string>
* @param napiVersion
*/
async function maybeGetKeytar(targets, napiVersion = 3) {
const target = Array.from(targets).filter(t => t !== process.platform)
if (target.length === 0 || process.env.JENKINS) return
console.log("fetching prebuilt keytar for", target, "N-API", napiVersion)
return Promise.all(target.map(t => exec(
`npx prebuild-install --platform ${t} --target ${napiVersion} --tag-prefix v --runtime napi --verbose`,
{
cwd: './node_modules/keytar/',
stdout: 'inherit'
}
)))
}
/**
* get the DLL that's needed for the windows client to handle "Send as Mail..." context
* menu actions.
* Tries to get a locally built version before delegating to downloadLatestMapirs
* @param distDir the directory to put the DLL
* @returns {Promise<void>}
*/
async function getMapirs(distDir) {
const dllName = "mapirs.dll"
const dllSrc = process.platform === "win32"
? path.join('../mapirs/target/x86_64-pc-windows-msvc/release', dllName)
: path.join('../mapirs/target/x86_64-pc-windows-gnu/release', dllName)
const dllTrg = path.join(distDir, dllName)
console.log("trying to copy", dllName, "from", dllSrc, "to", dllTrg)
try {
await fs.promises.copyFile(dllSrc, dllTrg)
} catch (e) {
console.log("no local", dllName, "found, using release from github")
await downloadLatestMapirs(dllName, dllTrg)
}
}
/**
* get the latest mapirs.dll release from github.
* @param dllName {string} name of the file that should be downloaded from the latest release
* @param dllTrg {string} path to put the downloaded file
* @returns {Promise<void>}
*/
async function downloadLatestMapirs(dllName, dllTrg) {
const {Octokit} = await import("@octokit/rest")
const octokit = new Octokit();
const opts = {
owner: "tutao",
repo: "mapirs"
}
const res = await octokit.request('GET /repos/{owner}/{repo}/releases/latest', opts)
const asset_id = res.data.assets.find(a => a.name.startsWith(dllName)).id
const asset = await octokit.repos.getReleaseAsset(Object.assign(opts, {
asset_id,
headers: {
"Accept": "application/octet-stream"
}
}))
await fs.promises.writeFile(dllTrg, Buffer.from(asset.data))
2019-09-13 13:49:11 +02:00
}