From cbcab5189502b9ed9b8504de7fb252205f6c9be6 Mon Sep 17 00:00:00 2001 From: Nikolay Velizhanin Date: Mon, 10 Apr 2023 18:00:36 +0500 Subject: [PATCH] feat(nsis): display required disk space #7487 --- .../src/targets/nsis/Defines.ts | 4 + .../src/targets/nsis/NsisTarget.ts | 6 +- .../src/targets/nsis/nsisUtil.ts | 32 +++++-- .../app-builder-lib/templates/nsis/common.nsh | 29 ++++++- .../templates/nsis/installer.nsi | 84 ++++++++++--------- packages/builder-util/src/fs.ts | 19 +++++ 6 files changed, 121 insertions(+), 53 deletions(-) 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..c1bce381016 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,30 @@ 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(() => dirSize(appOutDir)) + .then(unpackedSize => + target.buildAppPackage(appOutDir, arch).then(fileInfo => ({ + fileInfo, + unpackedSize, + })) + ) + 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..821e083e9aa 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,32 @@ 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 + SectionSetSize ${SECTION_ID} ${APP_32_UNPACKED_SIZE} + !endif +!macroend + !macro check64BitAndSetRegView # https://github.com/electron-userland/electron-builder/issues/2420 ${If} ${IsWin2000} @@ -107,7 +132,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..7413a92909b 100644 --- a/packages/app-builder-lib/templates/nsis/installer.nsi +++ b/packages/app-builder-lib/templates/nsis/installer.nsi @@ -39,50 +39,11 @@ Var oldMenuDirectory !insertmacro customHeader !endif -Function .onInit - SetOutPath $INSTDIR - ${LogSet} on - - !ifmacrodef preInit - !insertmacro preInit - !endif - - !ifdef DISPLAY_LANG_SELECTOR - !insertmacro MUI_LANGDLL_DISPLAY - !endif - - !ifdef BUILD_UNINSTALLER - WriteUninstaller "${UNINSTALLER_OUT_FILE}" - !insertmacro quitSuccess - !else - !insertmacro check64BitAndSetRegView - - !ifdef ONE_CLICK - !insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE - !else - ${IfNot} ${UAC_IsInnerInstance} - !insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE - ${EndIf} - !endif - - !insertmacro initMultiUser - - !ifmacrodef customInit - !insertmacro customInit - !endif - - !ifmacrodef addLicenseFiles - InitPluginsDir - !insertmacro addLicenseFiles - !endif - !endif -FunctionEnd - !ifndef BUILD_UNINSTALLER !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, @@ -116,4 +77,45 @@ SectionEnd !ifdef BUILD_UNINSTALLER !include "uninstaller.nsh" -!endif \ No newline at end of file +!endif + +Function .onInit + !insertmacro setSpaceRequired ${INSTALL_SECTION_ID} + + SetOutPath $INSTDIR + ${LogSet} on + + !ifmacrodef preInit + !insertmacro preInit + !endif + + !ifdef DISPLAY_LANG_SELECTOR + !insertmacro MUI_LANGDLL_DISPLAY + !endif + + !ifdef BUILD_UNINSTALLER + WriteUninstaller "${UNINSTALLER_OUT_FILE}" + !insertmacro quitSuccess + !else + !insertmacro check64BitAndSetRegView + + !ifdef ONE_CLICK + !insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE + !else + ${IfNot} ${UAC_IsInnerInstance} + !insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE + ${EndIf} + !endif + + !insertmacro initMultiUser + + !ifmacrodef customInit + !insertmacro customInit + !endif + + !ifmacrodef addLicenseFiles + InitPluginsDir + !insertmacro addLicenseFiles + !endif + !endif +FunctionEnd \ No newline at end of file diff --git a/packages/builder-util/src/fs.ts b/packages/builder-util/src/fs.ts index a5719fac93c..218a5226f87 100644 --- a/packages/builder-util/src/fs.ts +++ b/packages/builder-util/src/fs.ts @@ -306,6 +306,25 @@ 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