Skip to content

Commit a338730

Browse files
authored
feat: Allow for NSIS windows installer to be wrapped in an MSI (#7407)
1 parent ece7f88 commit a338730

File tree

20 files changed

+758
-112
lines changed

20 files changed

+758
-112
lines changed

.changeset/plenty-bees-give.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"app-builder-lib": patch
3+
---
4+
5+
feat: Allow for NSIS windows installer to be wrapped in an MSI

packages/app-builder-lib/scheme.json

+171
Original file line numberDiff line numberDiff line change
@@ -3699,6 +3699,167 @@
36993699
},
37003700
"type": "object"
37013701
},
3702+
"MsiWrappedOptions": {
3703+
"additionalProperties": false,
3704+
"properties": {
3705+
"additionalWixArgs": {
3706+
"anyOf": [
3707+
{
3708+
"items": {
3709+
"type": "string"
3710+
},
3711+
"type": "array"
3712+
},
3713+
{
3714+
"type": "null"
3715+
}
3716+
],
3717+
"description": "Any additional arguments to be passed to the WiX installer compiler, such as `[\"-ext\", \"WixUtilExtension\"]`"
3718+
},
3719+
"artifactName": {
3720+
"description": "The [artifact file name template](/configuration/configuration#artifact-file-name-template).",
3721+
"type": [
3722+
"null",
3723+
"string"
3724+
]
3725+
},
3726+
"createDesktopShortcut": {
3727+
"default": true,
3728+
"description": "Whether to create desktop shortcut. Set to `always` if to recreate also on reinstall (even if removed by user).",
3729+
"enum": [
3730+
"always",
3731+
false,
3732+
true
3733+
]
3734+
},
3735+
"createStartMenuShortcut": {
3736+
"default": true,
3737+
"description": "Whether to create start menu shortcut.",
3738+
"type": "boolean"
3739+
},
3740+
"impersonate": {
3741+
"default": false,
3742+
"description": "Determines if the wrapped installer should be executed with impersonation",
3743+
"type": "boolean"
3744+
},
3745+
"menuCategory": {
3746+
"default": false,
3747+
"description": "Whether to create submenu for start menu shortcut and program files directory. If `true`, company name will be used. Or string value.",
3748+
"type": [
3749+
"string",
3750+
"boolean"
3751+
]
3752+
},
3753+
"oneClick": {
3754+
"type": "boolean"
3755+
},
3756+
"perMachine": {
3757+
"default": false,
3758+
"description": "Whether to install per all users (per-machine).",
3759+
"type": "boolean"
3760+
},
3761+
"publish": {
3762+
"anyOf": [
3763+
{
3764+
"$ref": "#/definitions/GithubOptions"
3765+
},
3766+
{
3767+
"$ref": "#/definitions/S3Options"
3768+
},
3769+
{
3770+
"$ref": "#/definitions/SpacesOptions"
3771+
},
3772+
{
3773+
"$ref": "#/definitions/GenericServerOptions"
3774+
},
3775+
{
3776+
"$ref": "#/definitions/CustomPublishOptions"
3777+
},
3778+
{
3779+
"$ref": "#/definitions/KeygenOptions"
3780+
},
3781+
{
3782+
"$ref": "#/definitions/SnapStoreOptions"
3783+
},
3784+
{
3785+
"$ref": "#/definitions/BitbucketOptions"
3786+
},
3787+
{
3788+
"items": {
3789+
"anyOf": [
3790+
{
3791+
"$ref": "#/definitions/GithubOptions"
3792+
},
3793+
{
3794+
"$ref": "#/definitions/S3Options"
3795+
},
3796+
{
3797+
"$ref": "#/definitions/SpacesOptions"
3798+
},
3799+
{
3800+
"$ref": "#/definitions/GenericServerOptions"
3801+
},
3802+
{
3803+
"$ref": "#/definitions/CustomPublishOptions"
3804+
},
3805+
{
3806+
"$ref": "#/definitions/KeygenOptions"
3807+
},
3808+
{
3809+
"$ref": "#/definitions/SnapStoreOptions"
3810+
},
3811+
{
3812+
"$ref": "#/definitions/BitbucketOptions"
3813+
},
3814+
{
3815+
"type": "string"
3816+
}
3817+
]
3818+
},
3819+
"type": "array"
3820+
},
3821+
{
3822+
"type": [
3823+
"null",
3824+
"string"
3825+
]
3826+
}
3827+
]
3828+
},
3829+
"runAfterFinish": {
3830+
"default": true,
3831+
"description": "Whether to run the installed application after finish. For assisted installer corresponding checkbox will be removed.",
3832+
"type": "boolean"
3833+
},
3834+
"shortcutName": {
3835+
"description": "The name that will be used for all shortcuts. Defaults to the application name.",
3836+
"type": [
3837+
"null",
3838+
"string"
3839+
]
3840+
},
3841+
"upgradeCode": {
3842+
"description": "The [upgrade code](https://msdn.microsoft.com/en-us/library/windows/desktop/aa372375(v=vs.85).aspx). Optional, by default generated using app id.",
3843+
"type": [
3844+
"null",
3845+
"string"
3846+
]
3847+
},
3848+
"warningsAsErrors": {
3849+
"default": true,
3850+
"description": "If `warningsAsErrors` is `true` (default): treat warnings as errors. If `warningsAsErrors` is `false`: allow warnings.",
3851+
"type": "boolean"
3852+
},
3853+
"wrappedInstallerArgs": {
3854+
"description": "Extra arguments to provide to the wrapped installer (ie: /S for silent install)",
3855+
"type": [
3856+
"null",
3857+
"string"
3858+
]
3859+
}
3860+
},
3861+
"type": "object"
3862+
},
37023863
"NotarizeOptions": {
37033864
"additionalProperties": false,
37043865
"properties": {
@@ -6791,6 +6952,16 @@
67916952
],
67926953
"description": "MSI project created on disk - not packed into .msi package yet."
67936954
},
6955+
"msiWrapped": {
6956+
"anyOf": [
6957+
{
6958+
"$ref": "#/definitions/MsiWrappedOptions"
6959+
},
6960+
{
6961+
"type": "null"
6962+
}
6963+
]
6964+
},
67946965
"nodeGypRebuild": {
67956966
"default": false,
67966967
"description": "Whether to execute `node-gyp rebuild` before starting to package the app.\n\nDon't [use](https://github.com/electron-userland/electron-builder/issues/683#issuecomment-241214075) [npm](http://electron.atom.io/docs/tutorial/using-native-node-modules/#using-npm) (neither `.npmrc`) for configuring electron headers. Use `electron-builder node-gyp-rebuild` instead.",

packages/app-builder-lib/src/configuration.ts

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AppXOptions } from "./options/AppXOptions"
66
import { AppImageOptions, DebOptions, FlatpakOptions, LinuxConfiguration, LinuxTargetSpecificOptions } from "./options/linuxOptions"
77
import { DmgOptions, MacConfiguration, MasConfiguration } from "./options/macOptions"
88
import { MsiOptions } from "./options/MsiOptions"
9+
import { MsiWrappedOptions } from "./options/MsiWrappedOptions"
910
import { PkgOptions } from "./options/pkgOptions"
1011
import { PlatformSpecificBuildOptions } from "./options/PlatformSpecificBuildOptions"
1112
import { SnapOptions } from "./options/SnapOptions"
@@ -73,6 +74,8 @@ export interface Configuration extends PlatformSpecificBuildOptions {
7374
readonly appx?: AppXOptions | null
7475
/** @private */
7576
readonly msi?: MsiOptions | null
77+
/** @private */
78+
readonly msiWrapped?: MsiWrappedOptions | null
7679
readonly squirrelWindows?: SquirrelWindowsOptions | null
7780

7881
/**

packages/app-builder-lib/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export { PkgOptions, PkgBackgroundOptions, BackgroundAlignment, BackgroundScalin
3131
export { WindowsConfiguration } from "./options/winOptions"
3232
export { AppXOptions } from "./options/AppXOptions"
3333
export { MsiOptions } from "./options/MsiOptions"
34+
export { MsiWrappedOptions } from "./options/MsiWrappedOptions"
3435
export { CommonWindowsInstallerConfiguration } from "./options/CommonWindowsInstallerConfiguration"
3536
export { NsisOptions, NsisWebOptions, PortableOptions, CommonNsisOptions } from "./targets/nsis/nsisOptions"
3637
export { LinuxConfiguration, DebOptions, CommonLinuxOptions, LinuxTargetSpecificOptions, AppImageOptions, FlatpakOptions } from "./options/linuxOptions"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { TargetSpecificOptions } from "../core"
2+
import { CommonWindowsInstallerConfiguration } from "./CommonWindowsInstallerConfiguration"
3+
4+
export interface MsiWrappedOptions extends CommonWindowsInstallerConfiguration, TargetSpecificOptions {
5+
/**
6+
* Extra arguments to provide to the wrapped installer (ie: /S for silent install)
7+
*/
8+
readonly wrappedInstallerArgs?: string | null
9+
10+
/**
11+
* Determines if the wrapped installer should be executed with impersonation
12+
* @default false
13+
*/
14+
readonly impersonate?: boolean
15+
16+
/**
17+
* The [upgrade code](https://msdn.microsoft.com/en-us/library/windows/desktop/aa372375(v=vs.85).aspx). Optional, by default generated using app id.
18+
*/
19+
readonly upgradeCode?: string | null
20+
21+
/**
22+
* If `warningsAsErrors` is `true` (default): treat warnings as errors. If `warningsAsErrors` is `false`: allow warnings.
23+
* @default true
24+
*/
25+
readonly warningsAsErrors?: boolean
26+
27+
/**
28+
* Any additional arguments to be passed to the WiX installer compiler, such as `["-ext", "WixUtilExtension"]`
29+
*/
30+
readonly additionalWixArgs?: Array<string> | null
31+
}

packages/app-builder-lib/src/packager.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ export class Packager {
412412

413413
private async doBuild(): Promise<Map<Platform, Map<string, Target>>> {
414414
const taskManager = new AsyncTaskManager(this.cancellationToken)
415+
const syncTargetsIfAny = [] as Target[]
415416

416417
const platformToTarget = new Map<Platform, Map<string, Target>>()
417418
const createdOutDirs = new Set<string>()
@@ -446,11 +447,19 @@ export class Packager {
446447
}
447448

448449
for (const target of nameToTarget.values()) {
449-
taskManager.addTask(target.finishBuild())
450+
if (target.isAsyncSupported) {
451+
taskManager.addTask(target.finishBuild())
452+
} else {
453+
syncTargetsIfAny.push(target)
454+
}
450455
}
451456
}
452457

453458
await taskManager.awaitTasks()
459+
460+
for (const target of syncTargetsIfAny) {
461+
await target.finishBuild()
462+
}
454463
return platformToTarget
455464
}
456465

packages/app-builder-lib/src/targets/MsiTarget.ts

+35-27
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,24 @@ import { createStageDir, getWindowsInstallationDirName } from "./targetUtil"
2121
const ELECTRON_BUILDER_UPGRADE_CODE_NS_UUID = UUID.parse("d752fe43-5d44-44d5-9fc9-6dd1bf19d5cc")
2222
const ROOT_DIR_ID = "APPLICATIONFOLDER"
2323

24-
const projectTemplate = new Lazy<(data: any) => string>(async () => {
25-
const template = (await readFile(path.join(getTemplatePath("msi"), "template.xml"), "utf8"))
26-
.replace(/{{/g, "<%")
27-
.replace(/}}/g, "%>")
28-
.replace(/\${([^}]+)}/g, "<%=$1%>")
29-
return ejs.compile(template)
30-
})
31-
3224
// WiX doesn't support Mono, so, dontnet462 is required to be installed for wine (preinstalled in our bundled wine)
3325
export default class MsiTarget extends Target {
34-
private readonly vm = process.platform === "win32" ? new VmManager() : new WineVmManager()
26+
protected readonly vm = process.platform === "win32" ? new VmManager() : new WineVmManager()
3527

3628
readonly options: MsiOptions = deepAssign(this.packager.platformSpecificBuildOptions, this.packager.config.msi)
3729

38-
constructor(private readonly packager: WinPackager, readonly outDir: string) {
39-
super("msi")
30+
constructor(protected readonly packager: WinPackager, readonly outDir: string, name = "msi", isAsyncSupported = true) {
31+
super(name, isAsyncSupported)
4032
}
4133

34+
protected projectTemplate = new Lazy<(data: any) => string>(async () => {
35+
const template = (await readFile(path.join(getTemplatePath(this.name), "template.xml"), "utf8"))
36+
.replace(/{{/g, "<%")
37+
.replace(/}}/g, "%>")
38+
.replace(/\${([^}]+)}/g, "<%=$1%>")
39+
return ejs.compile(template)
40+
})
41+
4242
/**
4343
* A product-specific string that can be used in an [MSI Identifier](https://docs.microsoft.com/en-us/windows/win32/msi/identifier).
4444
*/
@@ -47,11 +47,11 @@ export default class MsiTarget extends Target {
4747
return sanitizedId.length > 0 ? sanitizedId : "App" + this.upgradeCode.replace(/-/g, "")
4848
}
4949

50-
private get iconId() {
50+
protected get iconId() {
5151
return `${this.productMsiIdPrefix}Icon.exe`
5252
}
5353

54-
private get upgradeCode(): string {
54+
protected get upgradeCode(): string {
5555
return (this.options.upgradeCode || UUID.v5(this.packager.appInfo.id, ELECTRON_BUILDER_UPGRADE_CODE_NS_UUID)).toUpperCase()
5656
}
5757

@@ -145,22 +145,36 @@ export default class MsiTarget extends Target {
145145
return args
146146
}
147147

148-
private async writeManifest(appOutDir: string, arch: Arch, commonOptions: FinalCommonWindowsInstallerOptions) {
148+
protected async writeManifest(appOutDir: string, arch: Arch, commonOptions: FinalCommonWindowsInstallerOptions) {
149149
const appInfo = this.packager.appInfo
150150
const { files, dirs } = await this.computeFileDeclaration(appOutDir)
151+
const options = this.options
152+
153+
return (await this.projectTemplate.value)({
154+
...(await this.getBaseOptions(commonOptions)),
155+
isCreateDesktopShortcut: commonOptions.isCreateDesktopShortcut !== DesktopShortcutCreationPolicy.NEVER,
156+
isRunAfterFinish: options.runAfterFinish !== false,
157+
// https://stackoverflow.com/questions/1929038/compilation-error-ice80-the-64bitcomponent-uses-32bitdirectory
158+
programFilesId: arch === Arch.x64 ? "ProgramFiles64Folder" : "ProgramFilesFolder",
159+
// wix in the name because special wix format can be used in the name
160+
installationDirectoryWixName: getWindowsInstallationDirName(appInfo, commonOptions.isAssisted || commonOptions.isPerMachine === true),
161+
dirs,
162+
files,
163+
})
164+
}
165+
166+
protected async getBaseOptions(commonOptions: FinalCommonWindowsInstallerOptions): Promise<any> {
167+
const appInfo = this.packager.appInfo
168+
const iconPath = await this.packager.getIconPath()
169+
const compression = this.packager.compression
151170

152171
const companyName = appInfo.companyName
153172
if (!companyName) {
154173
log.warn(`Manufacturer is not set for MSI — please set "author" in the package.json`)
155174
}
156175

157-
const compression = this.packager.compression
158-
const options = this.options
159-
const iconPath = await this.packager.getIconPath()
160-
return (await projectTemplate.value)({
176+
return {
161177
...commonOptions,
162-
isCreateDesktopShortcut: commonOptions.isCreateDesktopShortcut !== DesktopShortcutCreationPolicy.NEVER,
163-
isRunAfterFinish: options.runAfterFinish !== false,
164178
iconPath: iconPath == null ? null : this.vm.toVmFile(iconPath),
165179
iconId: this.iconId,
166180
compressionLevel: compression === "store" ? "none" : "high",
@@ -169,13 +183,7 @@ export default class MsiTarget extends Target {
169183
upgradeCode: this.upgradeCode,
170184
manufacturer: companyName || appInfo.productName,
171185
appDescription: appInfo.description,
172-
// https://stackoverflow.com/questions/1929038/compilation-error-ice80-the-64bitcomponent-uses-32bitdirectory
173-
programFilesId: arch === Arch.x64 ? "ProgramFiles64Folder" : "ProgramFilesFolder",
174-
// wix in the name because special wix format can be used in the name
175-
installationDirectoryWixName: getWindowsInstallationDirName(appInfo, commonOptions.isAssisted || commonOptions.isPerMachine === true),
176-
dirs,
177-
files,
178-
})
186+
}
179187
}
180188

181189
private async computeFileDeclaration(appOutDir: string) {

0 commit comments

Comments
 (0)