Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(msi): add fileAssociation support for MSI target #6530

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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