diff --git a/.changeset/modern-waves-compete.md b/.changeset/modern-waves-compete.md new file mode 100644 index 00000000000..23b259457c8 --- /dev/null +++ b/.changeset/modern-waves-compete.md @@ -0,0 +1,6 @@ +--- +"app-builder-lib": minor +"builder-util": minor +--- + +Display "Space required" text for NSIS installer diff --git a/packages/app-builder-lib/src/targets/nsis/Defines.ts b/packages/app-builder-lib/src/targets/nsis/Defines.ts index 07f78c3b7c0..5402f35b28e 100644 --- a/packages/app-builder-lib/src/targets/nsis/Defines.ts +++ b/packages/app-builder-lib/src/targets/nsis/Defines.ts @@ -44,6 +44,10 @@ export type Defines = { APP_ARM64_HASH?: string APP_32_HASH?: string + APP_64_UNPACKED_SIZE?: string + APP_ARM64_UNPACKED_SIZE?: string + APP_32_UNPACKED_SIZE?: string + REQUEST_EXECUTION_LEVEL?: PortableOptions["requestExecutionLevel"] UNPACK_DIR_NAME?: string | false diff --git a/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts b/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts index 6a9ecab56c1..706bbee6ed8 100644 --- a/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts +++ b/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts @@ -229,7 +229,7 @@ export class NsisTarget extends Target { defines.APP_BUILD_DIR = archs.get(archs.keys().next().value) } else { await BluebirdPromise.map(archs.keys(), async arch => { - const fileInfo = await this.packageHelper.packArch(arch, this) + const { fileInfo, unpackedSize } = await this.packageHelper.packArch(arch, this) const file = fileInfo.path const defineKey = arch === Arch.x64 ? "APP_64" : arch === Arch.arm64 ? "APP_ARM64" : "APP_32" defines[defineKey] = file @@ -240,6 +240,10 @@ export class NsisTarget extends Target { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const defineHashKey = `${defineKey}_HASH` as "APP_64_HASH" | "APP_ARM64_HASH" | "APP_32_HASH" defines[defineHashKey] = Buffer.from(fileInfo.sha512, "base64").toString("hex").toUpperCase() + // NSIS accepts size in KiloBytes and supports only whole numbers + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const defineUnpackedSizeKey = `${defineKey}_UNPACKED_SIZE` as "APP_64_UNPACKED_SIZE" | "APP_ARM64_UNPACKED_SIZE" | "APP_32_UNPACKED_SIZE" + defines[defineUnpackedSizeKey] = Math.ceil(unpackedSize / 1024).toString() if (this.isWebInstaller) { await packager.dispatchArtifactCreated(file, this, arch) diff --git a/packages/app-builder-lib/src/targets/nsis/nsisUtil.ts b/packages/app-builder-lib/src/targets/nsis/nsisUtil.ts index 4805f2aae2d..d3c9617c6b8 100644 --- a/packages/app-builder-lib/src/targets/nsis/nsisUtil.ts +++ b/packages/app-builder-lib/src/targets/nsis/nsisUtil.ts @@ -1,7 +1,7 @@ import { Arch, log } from "builder-util" import { PackageFileInfo } from "builder-util-runtime" import { getBinFromUrl, getBinFromCustomLoc } from "../../binDownload" -import { copyFile } from "builder-util/out/fs" +import { copyFile, dirSize } from "builder-util/out/fs" import * as path from "path" import { getTemplatePath } from "../../util/pathManager" import { NsisTarget } from "./NsisTarget" @@ -39,8 +39,13 @@ export const NSIS_PATH = () => { }) } +export interface PackArchResult { + fileInfo: PackageFileInfo + unpackedSize: number +} + export class AppPackageHelper { - private readonly archToFileInfo = new Map>() + private readonly archToResult = new Map>() private readonly infoToIsDelete = new Map() /** @private */ @@ -48,21 +53,28 @@ export class AppPackageHelper { constructor(private readonly elevateHelper: CopyElevateHelper) {} - async packArch(arch: Arch, target: NsisTarget): Promise { - let infoPromise = this.archToFileInfo.get(arch) - if (infoPromise == null) { + async packArch(arch: Arch, target: NsisTarget): Promise { + let resultPromise = this.archToResult.get(arch) + if (resultPromise == null) { const appOutDir = target.archs.get(arch)! - infoPromise = this.elevateHelper.copy(appOutDir, target).then(() => target.buildAppPackage(appOutDir, arch)) - this.archToFileInfo.set(arch, infoPromise) + resultPromise = this.elevateHelper + .copy(appOutDir, target) + .then(() => target.buildAppPackage(appOutDir, arch)) + .then(async fileInfo => ({ + fileInfo, + unpackedSize: await dirSize(appOutDir), + })) + this.archToResult.set(arch, resultPromise) } - const info = await infoPromise + const result = await resultPromise + const { fileInfo: info } = result if (target.isWebInstaller) { this.infoToIsDelete.set(info, false) } else if (!this.infoToIsDelete.has(info)) { this.infoToIsDelete.set(info, true) } - return info + return result } async finishBuild(): Promise { diff --git a/packages/app-builder-lib/templates/nsis/common.nsh b/packages/app-builder-lib/templates/nsis/common.nsh index 0509c033876..2aab0c3ffee 100644 --- a/packages/app-builder-lib/templates/nsis/common.nsh +++ b/packages/app-builder-lib/templates/nsis/common.nsh @@ -3,7 +3,6 @@ BrandingText "${PRODUCT_NAME} ${VERSION}" ShowInstDetails nevershow -SpaceTexts none !ifdef BUILD_UNINSTALLER ShowUninstDetails nevershow !endif @@ -13,6 +12,34 @@ Name "${PRODUCT_NAME}" !define APP_EXECUTABLE_FILENAME "${PRODUCT_FILENAME}.exe" !define UNINSTALL_FILENAME "Uninstall ${PRODUCT_FILENAME}.exe" +!macro setSpaceRequired SECTION_ID + !ifdef APP_64_UNPACKED_SIZE + !ifdef APP_32_UNPACKED_SIZE + !ifdef APP_ARM64_UNPACKED_SIZE + ${if} ${IsNativeARM64} + SectionSetSize ${SECTION_ID} ${APP_ARM64_UNPACKED_SIZE} + ${elseif} ${IsNativeAMD64} + SectionSetSize ${SECTION_ID} ${APP_64_UNPACKED_SIZE} + ${else} + SectionSetSize ${SECTION_ID} ${APP_32_UNPACKED_SIZE} + ${endif} + !else + ${if} ${RunningX64} + SectionSetSize ${SECTION_ID} ${APP_64_UNPACKED_SIZE} + ${else} + SectionSetSize ${SECTION_ID} ${APP_32_UNPACKED_SIZE} + ${endif} + !endif + !else + SectionSetSize ${SECTION_ID} ${APP_64_UNPACKED_SIZE} + !endif + !else + !ifdef APP_32_UNPACKED_SIZE + SectionSetSize ${SECTION_ID} ${APP_32_UNPACKED_SIZE} + !endif + !endif +!macroend + !macro check64BitAndSetRegView # https://github.com/electron-userland/electron-builder/issues/2420 ${If} ${IsWin2000} @@ -107,7 +134,7 @@ Name "${PRODUCT_NAME}" LogSet ${SETTING} !endif !macroend - + !define LogText "!insertmacro LogTextMacroEB" !macro LogTextMacroEB INPUT_TEXT !ifdef ENABLE_LOGGING_ELECTRON_BUILDER diff --git a/packages/app-builder-lib/templates/nsis/installer.nsi b/packages/app-builder-lib/templates/nsis/installer.nsi index 4c69e9a888a..1b88a9d50a6 100644 --- a/packages/app-builder-lib/templates/nsis/installer.nsi +++ b/packages/app-builder-lib/templates/nsis/installer.nsi @@ -40,6 +40,8 @@ Var oldMenuDirectory !endif Function .onInit + Call setInstallSectionSpaceRequired + SetOutPath $INSTDIR ${LogSet} on @@ -82,7 +84,7 @@ FunctionEnd !include "installUtil.nsh" !endif -Section "install" +Section "install" INSTALL_SECTION_ID !ifndef BUILD_UNINSTALLER # If we're running a silent upgrade of a per-machine installation, elevate so extracting the new app will succeed. # For a non-silent install, the elevation will be triggered when the install mode is selected in the UI, @@ -114,6 +116,10 @@ Section "install" !endif SectionEnd +Function setInstallSectionSpaceRequired + !insertmacro setSpaceRequired ${INSTALL_SECTION_ID} +FunctionEnd + !ifdef BUILD_UNINSTALLER !include "uninstaller.nsh" !endif \ No newline at end of file diff --git a/packages/builder-util/src/fs.ts b/packages/builder-util/src/fs.ts index a5719fac93c..0a67b4f63ee 100644 --- a/packages/builder-util/src/fs.ts +++ b/packages/builder-util/src/fs.ts @@ -306,6 +306,27 @@ export function copyDir(src: string, destination: string, options: CopyDirOption }).then(() => BluebirdPromise.map(links, it => symlink(it.link, it.file, symlinkType), CONCURRENCY)) } +export async function dirSize(dirPath: string): Promise { + const entries = await readdir(dirPath, { withFileTypes: true }) + + const entrySizes = entries.map(async entry => { + const entryPath = path.join(dirPath, entry.name) + + if (entry.isDirectory()) { + return await dirSize(entryPath) + } + + if (entry.isFile()) { + const { size } = await stat(entryPath) + return size + } + + return 0 + }) + + return (await Promise.all(entrySizes)).reduce((entrySize, totalSize) => entrySize + totalSize, 0) +} + // eslint-disable-next-line @typescript-eslint/no-unused-vars export const DO_NOT_USE_HARD_LINKS = (file: string) => false // eslint-disable-next-line @typescript-eslint/no-unused-vars