Skip to content

Commit

Permalink
feat(msi): add fileAssociation support for MSI target (#6530)
Browse files Browse the repository at this point in the history
fix(win): iconId sometimes containing invalid characters, and iconId config option being ignored.
fix(msi): change the fallback value for generated MSI Ids to a unique string for the product.

BREAKING CHANGE: remove MSI option `iconId`
  • Loading branch information
aplum authored Jan 12, 2022
1 parent c5f8b08 commit 04a3f14
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/giant-dryers-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"app-builder-lib": minor
---

feat(msi): add fileAssociation support for MSI target
5 changes: 5 additions & 0 deletions .changeset/serious-peas-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"app-builder-lib": major
---

BREAKING CHANGE: remove MSI option `iconId`
6 changes: 3 additions & 3 deletions packages/app-builder-lib/src/options/FileAssociation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* File associations.
*
* macOS (corresponds to [CFBundleDocumentTypes](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-101685)) and NSIS only.
* macOS (corresponds to [CFBundleDocumentTypes](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-101685)), NSIS, and MSI only.
*
* On Windows works only if [nsis.perMachine](https://electron.build/configuration/configuration#NsisOptions-perMachine) is set to `true`.
* On Windows (NSIS) works only if [nsis.perMachine](https://electron.build/configuration/configuration#NsisOptions-perMachine) is set to `true`.
*/
export interface FileAssociation {
/**
Expand All @@ -29,7 +29,7 @@ export interface FileAssociation {
/**
* The path to icon (`.icns` for MacOS and `.ico` for Windows), relative to `build` (build resources directory). Defaults to `${firstExt}.icns`/`${firstExt}.ico` (if several extensions specified, first is used) or to application icon.
*
* Not supported on Linux, file issue if need (default icon will be `x-office-document`).
* Not supported on Linux, file issue if need (default icon will be `x-office-document`). Not supported on MSI.
*/
readonly icon?: string | null

Expand Down
5 changes: 0 additions & 5 deletions packages/app-builder-lib/src/options/MsiOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,4 @@ export interface MsiOptions extends CommonWindowsInstallerConfiguration, TargetS
* Any additional arguments to be passed to the WiX installer compiler, such as `["-ext", "WixUtilExtension"]`
*/
readonly additionalWixArgs?: Array<string> | null

/**
* The [shortcut iconId](https://wixtoolset.org/documentation/manual/v4/reference/wxs/shortcut/). Optional, by default generated using app file name.
*/
readonly iconId?: string
}
45 changes: 38 additions & 7 deletions packages/app-builder-lib/src/targets/MsiTarget.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import BluebirdPromise from "bluebird-lst"
import { Arch, log, deepAssign } from "builder-util"
import { Arch, asArray, log, deepAssign } from "builder-util"
import { UUID } from "builder-util-runtime"
import { getBinFromUrl } from "../binDownload"
import { walk } from "builder-util/out/fs"
Expand All @@ -11,6 +11,7 @@ import * as path from "path"
import { MsiOptions } from "../"
import { Target } from "../core"
import { DesktopShortcutCreationPolicy, FinalCommonWindowsInstallerOptions, getEffectiveOptions } from "../options/CommonWindowsInstallerConfiguration"
import { normalizeExt } from "../platformPackager"
import { getTemplatePath } from "../util/pathManager"
import { VmManager } from "../vm/vm"
import { WineVmManager } from "../vm/WineVm"
Expand Down Expand Up @@ -40,6 +41,22 @@ export default class MsiTarget extends Target {
super("msi")
}

/**
* A product-specific string that can be used in an [MSI Identifier](https://docs.microsoft.com/en-us/windows/win32/msi/identifier).
*/
private get productMsiIdPrefix() {
const sanitizedId = this.packager.appInfo.productFilename.replace(/[^\w.]/g, "").replace(/^[^A-Za-z_]+/, "")
return sanitizedId.length > 0 ? sanitizedId : "App" + this.upgradeCode.replace(/-/g, "")
}

private get iconId() {
return `${this.productMsiIdPrefix}Icon.exe`
}

private get upgradeCode(): string {
return (this.options.upgradeCode || UUID.v5(this.packager.appInfo.id, ELECTRON_BUILDER_UPGRADE_CODE_NS_UUID)).toUpperCase()
}

async build(appOutDir: string, arch: Arch) {
const packager = this.packager
const artifactName = packager.expandArtifactBeautyNamePattern(this.options, "msi", arch)
Expand Down Expand Up @@ -156,17 +173,16 @@ export default class MsiTarget extends Target {
const compression = this.packager.compression
const options = this.options
const iconPath = await this.packager.getIconPath()
const iconId = `${appInfo.productFilename}Icon.exe`.replace(/\s/g, "")
return (await projectTemplate.value)({
...commonOptions,
isCreateDesktopShortcut: commonOptions.isCreateDesktopShortcut !== DesktopShortcutCreationPolicy.NEVER,
isRunAfterFinish: options.runAfterFinish !== false,
iconPath: iconPath == null ? null : this.vm.toVmFile(iconPath),
iconId: iconId,
iconId: this.iconId,
compressionLevel: compression === "store" ? "none" : "high",
version: appInfo.getVersionInWeirdWindowsForm(),
productName: appInfo.productName,
upgradeCode: (options.upgradeCode || UUID.v5(appInfo.id, ELECTRON_BUILDER_UPGRADE_CODE_NS_UUID)).toUpperCase(),
upgradeCode: this.upgradeCode,
manufacturer: companyName || appInfo.productName,
appDescription: appInfo.description,
// https://stackoverflow.com/questions/1929038/compilation-error-ice80-the-64bitcomponent-uses-32bitdirectory
Expand Down Expand Up @@ -223,9 +239,8 @@ export default class MsiTarget extends Target {
if (isMainExecutable && (isCreateDesktopShortcut || commonOptions.isCreateStartMenuShortcut)) {
result += `>\n`
const shortcutName = commonOptions.shortcutName
const iconId = `${appInfo.productFilename}Icon.exe`.replace(/\s/g, "")
if (isCreateDesktopShortcut) {
result += `${fileSpace} <Shortcut Id="desktopShortcut" Directory="DesktopFolder" Name="${shortcutName}" WorkingDirectory="APPLICATIONFOLDER" Advertise="yes" Icon="${iconId}"/>\n`
result += `${fileSpace} <Shortcut Id="desktopShortcut" Directory="DesktopFolder" Name="${shortcutName}" WorkingDirectory="APPLICATIONFOLDER" Advertise="yes" Icon="${this.iconId}"/>\n`
}

const hasMenuCategory = commonOptions.menuCategory != null
Expand All @@ -234,7 +249,7 @@ export default class MsiTarget extends Target {
if (hasMenuCategory) {
dirs.push(`<Directory Id="${startMenuShortcutDirectoryId}" Name="ProgramMenuFolder:\\${commonOptions.menuCategory}\\"/>`)
}
result += `${fileSpace} <Shortcut Id="startMenuShortcut" Directory="${startMenuShortcutDirectoryId}" Name="${shortcutName}" WorkingDirectory="APPLICATIONFOLDER" Advertise="yes" Icon="${iconId}">\n`
result += `${fileSpace} <Shortcut Id="startMenuShortcut" Directory="${startMenuShortcutDirectoryId}" Name="${shortcutName}" WorkingDirectory="APPLICATIONFOLDER" Advertise="yes" Icon="${this.iconId}">\n`
result += `${fileSpace} <ShortcutProperty Key="System.AppUserModel.ID" Value="${this.packager.appInfo.id}"/>\n`
result += `${fileSpace} </Shortcut>\n`
}
Expand All @@ -247,6 +262,22 @@ export default class MsiTarget extends Target {
result += `/>`
}

const fileAssociations = this.packager.fileAssociations
if (isMainExecutable && fileAssociations.length !== 0) {
for (const item of fileAssociations) {
const extensions = asArray(item.ext).map(normalizeExt)
for (const ext of extensions) {
result += `${fileSpace} <ProgId Id="${this.productMsiIdPrefix}.${ext}" Advertise="yes" Icon="${this.iconId}" ${
item.description ? `Description="${item.description}"` : ""
}>\n`
result += `${fileSpace} <Extension Id="${ext}" Advertise="yes">\n`
result += `${fileSpace} <Verb Id="open" Command="Open with ${this.packager.appInfo.productName}" Argument="&quot;%1&quot;"/>\n`
result += `${fileSpace} </Extension>\n`
result += `${fileSpace} </ProgId>\n`
}
}
}

return `${result}\n${fileSpace}</Component>`
})

Expand Down

0 comments on commit 04a3f14

Please sign in to comment.