import * as electronRebuild from "@electron/rebuild"
import { RebuildMode } from "@electron/rebuild/lib/types"
import { asArray, log, spawn } from "builder-util"
import { pathExists } from "fs-extra"
import { Lazy } from "lazy-val"
import { homedir } from "os"
import * as path from "path"
import { Configuration } from "../configuration"
import { executeAppBuilderAndWriteJson } from "./appBuilder"
import { PM, detect, getPackageManagerVersion } from "../node-module-collector"
import { NodeModuleDirInfo } from "./packageDependencies"
import { rebuild as remoteRebuild } from "./rebuild/rebuild"
export async function installOrRebuild(config: Configuration, { appDir, projectDir }: DirectoryPaths, options: RebuildOptions, forceInstall = false) {
const effectiveOptions: RebuildOptions = {
buildFromSource: config.buildDependenciesFromSource === true,
additionalArgs: asArray(config.npmArgs),
...options,
}
let isDependenciesInstalled = false
for (const fileOrDir of ["node_modules", ".pnp.js"]) {
if (await pathExists(path.join(appDir, fileOrDir))) {
isDependenciesInstalled = true
break
}
}
if (forceInstall || !isDependenciesInstalled) {
await installDependencies(config, { appDir, projectDir }, effectiveOptions)
} else {
await rebuild(config, { appDir, projectDir }, effectiveOptions)
}
}
export interface DesktopFrameworkInfo {
version: string
useCustomDist: boolean
}
function getElectronGypCacheDir() {
return path.join(homedir(), ".electron-gyp")
}
export function getGypEnv(frameworkInfo: DesktopFrameworkInfo, platform: NodeJS.Platform, arch: string, buildFromSource: boolean) {
const npmConfigArch = arch === "armv7l" ? "arm" : arch
const common: any = {
...process.env,
npm_config_arch: npmConfigArch,
npm_config_target_arch: npmConfigArch,
npm_config_platform: platform,
npm_config_build_from_source: buildFromSource,
npm_config_target_platform: platform,
npm_config_update_binary: true,
npm_config_fallback_to_build: true,
}
if (platform !== process.platform) {
common.npm_config_force = "true"
}
if (platform === "win32" || platform === "darwin") {
common.npm_config_target_libc = "unknown"
}
if (!frameworkInfo.useCustomDist) {
return common
}
return {
...common,
npm_config_disturl: common.npm_config_electron_mirror || "https://electronjs.org/headers",
npm_config_target: frameworkInfo.version,
npm_config_runtime: "electron",
npm_config_devdir: getElectronGypCacheDir(),
}
}
async function checkYarnBerry(pm: PM) {
if (pm !== "yarn") {
return false
}
const version = await getPackageManagerVersion(pm)
if (version == null || version.split(".").length < 1) {
return false
}
return version.split(".")[0] >= "2"
}
async function installDependencies(config: Configuration, { appDir, projectDir }: DirectoryPaths, options: RebuildOptions): Promise<any> {
const platform = options.platform || process.platform
const arch = options.arch || process.arch
const additionalArgs = options.additionalArgs
const pm = await detect({ cwd: projectDir })
log.info({ pm, platform, arch, projectDir, appDir }, `installing production dependencies`)
const execArgs = ["install"]
const isYarnBerry = await checkYarnBerry(pm)
if (!isYarnBerry) {
if (process.env.NPM_NO_BIN_LINKS === "true") {
execArgs.push("--no-bin-links")
}
}
if (!isRunningYarn(pm)) {
execArgs.push("--prefer-offline")
}
const execPath = getPackageToolPath(pm)
if (additionalArgs != null) {
execArgs.push(...additionalArgs)
}
await spawn(execPath, execArgs, {
cwd: appDir,
env: getGypEnv(options.frameworkInfo, platform, arch, options.buildFromSource === true),
})
return rebuild(config, { appDir, projectDir }, options)
}
export async function nodeGypRebuild(platform: NodeJS.Platform, arch: string, frameworkInfo: DesktopFrameworkInfo) {
log.info({ platform, arch }, "executing node-gyp rebuild")
const nodeGyp = `node-gyp${process.platform === "win32" ? ".cmd" : ""}`
const args = ["rebuild"]
const [major, minor] = frameworkInfo.version
.split(".")
.slice(0, 2)
.map(n => parseInt(n, 10))
if (major <= 13 || (major == 14 && minor <= 1) || (major == 15 && minor <= 2)) {
args.push("--force-process-config")
}
await spawn(nodeGyp, args, { env: getGypEnv(frameworkInfo, platform, arch, true) })
}
function getPackageToolPath(pm: PM) {
let cmd = pm
if (process.env.FORCE_YARN === "true") {
cmd = "yarn"
}
return `${cmd}${process.platform === "win32" ? ".cmd" : ""}`
}
function isRunningYarn(pm: PM) {
const userAgent = process.env.npm_config_user_agent
return process.env.FORCE_YARN === "true" || pm === "yarn" || (userAgent != null && /\byarn\b/.test(userAgent))
}
export interface RebuildOptions {
frameworkInfo: DesktopFrameworkInfo
productionDeps: Lazy<Array<NodeModuleDirInfo>>
platform?: NodeJS.Platform
arch?: string
buildFromSource?: boolean
additionalArgs?: Array<string> | null
}
export interface DirectoryPaths {
appDir: string
projectDir: string
}
export async function rebuild(config: Configuration, { appDir, projectDir }: DirectoryPaths, options: RebuildOptions) {
const configuration = {
dependencies: await options.productionDeps.value,
nodeExecPath: process.execPath,
platform: options.platform || process.platform,
arch: options.arch || process.arch,
additionalArgs: options.additionalArgs,
execPath: process.env.npm_execpath || process.env.NPM_CLI_JS,
buildFromSource: options.buildFromSource === true,
}
const { arch, buildFromSource, platform } = configuration
if (config.nativeRebuilder === "legacy") {
const env = getGypEnv(options.frameworkInfo, platform, arch, buildFromSource)
return executeAppBuilderAndWriteJson(["rebuild-node-modules"], configuration, { env, cwd: appDir })
}
const {
frameworkInfo: { version: electronVersion },
} = options
const logInfo = {
electronVersion,
arch,
buildFromSource,
appDir: log.filePath(appDir) || "./",
}
log.info(logInfo, "executing @electron/rebuild")
const rebuildOptions: electronRebuild.RebuildOptions = {
buildPath: appDir,
electronVersion,
arch,
platform,
buildFromSource,
projectRootPath: projectDir,
mode: (config.nativeRebuilder as RebuildMode) || "sequential",
disablePreGypCopy: true,
}
return remoteRebuild(rebuildOptions)
}