diff --git a/packages/app-builder-lib/scheme.json b/packages/app-builder-lib/scheme.json index accd908b5de..e119fb732aa 100644 --- a/packages/app-builder-lib/scheme.json +++ b/packages/app-builder-lib/scheme.json @@ -1341,7 +1341,7 @@ "description": "The executable parameters. Pass to executableName" }, "executableName": { - "description": "The executable name. Defaults to `productName`.\nCannot be specified per target, allowed only in the `linux`.", + "description": "The executable name. Defaults to `productName`.", "type": [ "null", "string" @@ -1829,6 +1829,13 @@ "string" ] }, + "executableName": { + "description": "The executable name. Defaults to `productName`.", + "type": [ + "null", + "string" + ] + }, "asar": { "anyOf": [ { @@ -5013,6 +5020,13 @@ "string" ] }, + "executableName": { + "description": "The executable name. Defaults to `productName`.", + "type": [ + "null", + "string" + ] + }, "asar": { "anyOf": [ { diff --git a/packages/app-builder-lib/src/appInfo.ts b/packages/app-builder-lib/src/appInfo.ts index 3cb6090d27e..ab3d0aac1b8 100644 --- a/packages/app-builder-lib/src/appInfo.ts +++ b/packages/app-builder-lib/src/appInfo.ts @@ -29,6 +29,7 @@ export class AppInfo { readonly buildVersion: string readonly productName: string + readonly sanitizedProductName: string readonly productFilename: string constructor(private readonly info: Packager, buildVersion: string | null | undefined, private readonly platformSpecificOptions: PlatformSpecificBuildOptions | null = null) { @@ -55,7 +56,10 @@ export class AppInfo { } this.productName = info.config.productName || info.metadata.productName || info.metadata.name!! - this.productFilename = sanitizeFileName(this.productName) + this.sanitizedProductName = sanitizeFileName(this.productName) + this.productFilename = platformSpecificOptions?.executableName != null + ? sanitizeFileName(platformSpecificOptions.executableName) + : this.sanitizedProductName } get channel(): string | null { @@ -118,7 +122,7 @@ export class AppInfo { get linuxPackageName(): string { const name = this.name // https://github.com/electron-userland/electron-builder/issues/2963 - return name.startsWith("@") ? this.productFilename : name + return name.startsWith("@") ? this.sanitizedProductName : name } get sanitizedName(): string { diff --git a/packages/app-builder-lib/src/options/CommonWindowsInstallerConfiguration.ts b/packages/app-builder-lib/src/options/CommonWindowsInstallerConfiguration.ts index 300b71c7fe6..dfd69e69dcb 100644 --- a/packages/app-builder-lib/src/options/CommonWindowsInstallerConfiguration.ts +++ b/packages/app-builder-lib/src/options/CommonWindowsInstallerConfiguration.ts @@ -73,7 +73,7 @@ export function getEffectiveOptions(options: CommonWindowsInstallerConfiguration isPerMachine: options.perMachine === true, isAssisted: options.oneClick === false, - shortcutName: isEmptyOrSpaces(options.shortcutName) ? appInfo.productFilename : packager.expandMacro(options.shortcutName!!), + shortcutName: isEmptyOrSpaces(options.shortcutName) ? appInfo.sanitizedProductName : packager.expandMacro(options.shortcutName!!), isCreateDesktopShortcut: convertToDesktopShortcutCreationPolicy(options.createDesktopShortcut), isCreateStartMenuShortcut: options.createStartMenuShortcut !== false, menuCategory, diff --git a/packages/app-builder-lib/src/options/PlatformSpecificBuildOptions.ts b/packages/app-builder-lib/src/options/PlatformSpecificBuildOptions.ts index 268a4f209a4..65caea2692c 100644 --- a/packages/app-builder-lib/src/options/PlatformSpecificBuildOptions.ts +++ b/packages/app-builder-lib/src/options/PlatformSpecificBuildOptions.ts @@ -40,6 +40,11 @@ export interface PlatformSpecificBuildOptions extends TargetSpecificOptions { */ readonly artifactName?: string | null + /** + * The executable name. Defaults to `productName`. + */ + readonly executableName?: string | null + /** * The compression level. If you want to rapidly test build, `store` can reduce build time significantly. `maximum` doesn't lead to noticeable size difference, but increase build time. * @default normal diff --git a/packages/app-builder-lib/src/options/linuxOptions.ts b/packages/app-builder-lib/src/options/linuxOptions.ts index eab0d7f288e..f08398d635e 100644 --- a/packages/app-builder-lib/src/options/linuxOptions.ts +++ b/packages/app-builder-lib/src/options/linuxOptions.ts @@ -21,12 +21,6 @@ export interface LinuxConfiguration extends CommonLinuxOptions, PlatformSpecific */ readonly vendor?: string | null - /** - * The executable name. Defaults to `productName`. - * Cannot be specified per target, allowed only in the `linux`. - */ - readonly executableName?: string | null - /** * The path to icon set directory or one png file, relative to the [build resources](/configuration/configuration#MetadataDirectories-buildResources) or to the project directory. The icon filename must contain the size (e.g. 32x32.png) of the icon. * By default will be generated automatically based on the macOS icns file. diff --git a/packages/app-builder-lib/src/targets/LinuxTargetHelper.ts b/packages/app-builder-lib/src/targets/LinuxTargetHelper.ts index 9674910e0ef..5898cdeb8d8 100644 --- a/packages/app-builder-lib/src/targets/LinuxTargetHelper.ts +++ b/packages/app-builder-lib/src/targets/LinuxTargetHelper.ts @@ -94,10 +94,9 @@ export class LinuxTargetHelper { const packager = this.packager const appInfo = packager.appInfo - const productFilename = appInfo.productFilename const executableArgs = targetSpecificOptions.executableArgs if (exec == null) { - exec = `${installPrefix}/${productFilename}/${packager.executableName}` + exec = `${installPrefix}/${appInfo.sanitizedProductName}/${packager.executableName}` if (!/^[/0-9A-Za-z._-]+$/.test(exec)) { exec = `"${exec}"` } diff --git a/packages/app-builder-lib/src/targets/fpm.ts b/packages/app-builder-lib/src/targets/fpm.ts index 2766924264b..690e7fb1ef6 100644 --- a/packages/app-builder-lib/src/targets/fpm.ts +++ b/packages/app-builder-lib/src/targets/fpm.ts @@ -178,7 +178,7 @@ export default class FpmTarget extends Target { use(options.fpm, it => args.push(...it as any)) - args.push(`${appOutDir}/=${installPrefix}/${appInfo.productFilename}`) + args.push(`${appOutDir}/=${installPrefix}/${appInfo.sanitizedProductName}`) for (const icon of (await this.helper.icons)) { const extWithDot = path.extname(icon.file) const sizeName = extWithDot === ".svg" ? "scalable" : `${icon.size}x${icon.size}` diff --git a/packages/app-builder-lib/src/util/macroExpander.ts b/packages/app-builder-lib/src/util/macroExpander.ts index 4cccacd49f4..a530ab8f27e 100644 --- a/packages/app-builder-lib/src/util/macroExpander.ts +++ b/packages/app-builder-lib/src/util/macroExpander.ts @@ -17,7 +17,7 @@ export function expandMacro(pattern: string, arch: string | null | undefined, ap return pattern.replace(/\${([_a-zA-Z./*]+)}/g, (match, p1): string => { switch (p1) { case "productName": - return isProductNameSanitized ? appInfo.productFilename : appInfo.productName + return isProductNameSanitized ? appInfo.sanitizedProductName : appInfo.productName case "arch": if (arch == null) { diff --git a/test/snapshots/mac/macArchiveTest.js.snap b/test/snapshots/mac/macArchiveTest.js.snap index 48d9c95c784..2244a81c2ac 100644 --- a/test/snapshots/mac/macArchiveTest.js.snap +++ b/test/snapshots/mac/macArchiveTest.js.snap @@ -106,7 +106,7 @@ Object { exports[`invalid target 1`] = ` "Invalid configuration object. electron-builder has been initialized using a configuration object that does not match the API schema. - configuration.mac should be one of these: - object { appId?, artifactName?, asar?, asarUnpack?, binaries?, bundleShortVersion?, bundleVersion?, category?, compression?, cscInstallerKeyPassword?, cscInstallerLink?, cscKeyPassword?, cscLink?, darkModeSupport?, detectUpdateChannel?, electronLanguages?, electronUpdaterCompatibility?, entitlements?, entitlementsInherit?, entitlementsLoginHelper?, extendInfo?, extraDistFiles?, extraFiles?, extraResources?, fileAssociations?, files?, forceCodeSigning?, gatekeeperAssess?, generateUpdatesFilesForAllChannels?, hardenedRuntime?, helperBundleId?, helperEHBundleId?, helperGPUBundleId?, helperNPBundleId?, helperPluginBundleId?, helperRendererBundleId?, icon?, identity?, minimumSystemVersion?, protocols?, provisioningProfile?, publish?, releaseInfo?, requirements?, signIgnore?, strictVerify?, target?, type? } | null + object { appId?, artifactName?, executableName?, asar?, asarUnpack?, binaries?, bundleShortVersion?, bundleVersion?, category?, compression?, cscInstallerKeyPassword?, cscInstallerLink?, cscKeyPassword?, cscLink?, darkModeSupport?, detectUpdateChannel?, electronLanguages?, electronUpdaterCompatibility?, entitlements?, entitlementsInherit?, entitlementsLoginHelper?, extendInfo?, extraDistFiles?, extraFiles?, extraResources?, fileAssociations?, files?, forceCodeSigning?, gatekeeperAssess?, generateUpdatesFilesForAllChannels?, hardenedRuntime?, helperBundleId?, helperEHBundleId?, helperGPUBundleId?, helperNPBundleId?, helperPluginBundleId?, helperRendererBundleId?, icon?, identity?, minimumSystemVersion?, protocols?, provisioningProfile?, publish?, releaseInfo?, requirements?, signIgnore?, strictVerify?, target?, type? } | null -> Options related to how build macOS targets. Details: * configuration.mac.target[0] should be one of these: diff --git a/test/snapshots/windows/oneClickInstallerTest.js.snap b/test/snapshots/windows/oneClickInstallerTest.js.snap index c6304acd7ef..dc798ccb59f 100644 --- a/test/snapshots/windows/oneClickInstallerTest.js.snap +++ b/test/snapshots/windows/oneClickInstallerTest.js.snap @@ -36,6 +36,48 @@ Object { } `; +exports[`custom exec name 1`] = ` +Object { + "APP_32_NAME": undefined, + "APP_64_NAME": "TestApp-1.1.0-x64.nsis.7z", + "APP_ARM64_NAME": undefined, + "APP_FILENAME": "TestApp", + "APP_ID": "org.electron-builder.testApp", + "APP_PACKAGE_NAME": "TestApp", + "APP_PRODUCT_FILENAME": "Boo", + "COMPANY_NAME": "Foo Bar", + "ONE_CLICK": null, + "PRODUCT_FILENAME": "Boo", + "PRODUCT_NAME": "foo", + "SHORTCUT_NAME": "foo", + "UNINSTALL_DISPLAY_NAME": "foo 1.1.0", +} +`; + +exports[`custom exec name 2`] = ` +Object { + "win": Array [ + Object { + "arch": "x64", + "file": "foo Setup 1.1.0.exe", + "safeArtifactName": "foo-Setup-1.1.0.exe", + "updateInfo": Object { + "sha512": "@sha512", + "size": "@size", + }, + }, + Object { + "file": "foo Setup 1.1.0.exe.blockmap", + "safeArtifactName": "foo-Setup-1.1.0.exe.blockmap", + "updateInfo": Object { + "sha512": "@sha512", + "size": "@size", + }, + }, + ], +} +`; + exports[`custom guid 1`] = ` Object { "win": Array [ diff --git a/test/src/windows/oneClickInstallerTest.ts b/test/src/windows/oneClickInstallerTest.ts index a93c656871b..36a0f07b3c0 100644 --- a/test/src/windows/oneClickInstallerTest.ts +++ b/test/src/windows/oneClickInstallerTest.ts @@ -7,6 +7,24 @@ import { checkHelpers, doTest, expectUpdateMetadata } from "../helpers/winHelper const nsisTarget = Platform.WINDOWS.createTarget(["nsis"]) +function pickSnapshotDefines(defines: any) { + return { + "APP_32_NAME": defines.APP_32_NAME, + "APP_64_NAME": defines.APP_64_NAME, + "APP_ARM64_NAME": defines.APP_ARM64_NAME, + "APP_FILENAME": defines.APP_FILENAME, + "APP_ID": defines.APP_ID, + "APP_PACKAGE_NAME": defines.APP_PACKAGE_NAME, + "APP_PRODUCT_FILENAME": defines.APP_PRODUCT_FILENAME, + "COMPANY_NAME": defines.COMPANY_NAME, + "ONE_CLICK": defines.ONE_CLICK, + "PRODUCT_FILENAME": defines.PRODUCT_FILENAME, + "PRODUCT_NAME": defines.PRODUCT_NAME, + "SHORTCUT_NAME": defines.SHORTCUT_NAME, + "UNINSTALL_DISPLAY_NAME": defines.UNINSTALL_DISPLAY_NAME, + }; +} + test("one-click", app({ targets: Platform.WINDOWS.createTarget(["nsis"], Arch.x64), config: { @@ -232,4 +250,18 @@ test.ifDevOrLinuxCi("file associations per user", app({ } ], }, +})) + +test.ifWindows("custom exec name", app({ + targets: nsisTarget, + config: { + productName: "foo", + win: { + executableName: "Boo", + }, + }, + effectiveOptionComputed: async it => { + expect(pickSnapshotDefines(it[0])).toMatchSnapshot() + return false + } })) \ No newline at end of file