import { asArray, exists, isEmptyOrSpaces, log } from "builder-util"
import { outputFile } from "fs-extra"
import { Lazy } from "lazy-val"
import { join } from "path"
import { LinuxPackager } from "../linuxPackager"
import { LinuxTargetSpecificOptions } from "../options/linuxOptions"
import { IconInfo } from "../platformPackager"
export const installPrefix = "/opt"
export class LinuxTargetHelper {
private readonly iconPromise = new Lazy(() => this.computeDesktopIcons())
private readonly mimeTypeFilesPromise = new Lazy(() => this.computeMimeTypeFiles())
maxIconPath: string | null = null
constructor(private packager: LinuxPackager) {}
get icons(): Promise<Array<IconInfo>> {
return this.iconPromise.value
}
get mimeTypeFiles(): Promise<string | null> {
return this.mimeTypeFilesPromise.value
}
private async computeMimeTypeFiles(): Promise<string | null> {
const items: Array<string> = []
for (const fileAssociation of this.packager.fileAssociations) {
if (!fileAssociation.mimeType) {
continue
}
const data = `<mime-type type="${fileAssociation.mimeType}">
<glob pattern="*.${fileAssociation.ext}"/>
${fileAssociation.description ? `<comment>${fileAssociation.description}</comment>` : ""}
<icon name="x-office-document" />
</mime-type>`
items.push(data)
}
if (items.length === 0) {
return null
}
const file = await this.packager.getTempFile(".xml")
await outputFile(
file,
'<?xml version="1.0" encoding="utf-8"?>\n<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">\n' + items.join("\n") + "\n</mime-info>"
)
return file
}
private async computeDesktopIcons(): Promise<Array<IconInfo>> {
const packager = this.packager
const { platformSpecificBuildOptions, config } = packager
const sources = [platformSpecificBuildOptions.icon, config.mac?.icon ?? config.icon].filter(str => !!str) as string[]
let fallbackSources = [...asArray(packager.getDefaultFrameworkIcon())]
const buildResources = config.directories?.buildResources
if (buildResources && (await exists(join(buildResources, "icons")))) {
fallbackSources = [buildResources, ...fallbackSources]
}
const result = await packager.resolveIcon(sources, fallbackSources, "set")
this.maxIconPath = result[result.length - 1].file
return result
}
getDescription(options: LinuxTargetSpecificOptions) {
return options.description || this.packager.appInfo.description
}
getSanitizedVersion(target: string) {
const {
appInfo: { version },
} = this.packager
switch (target) {
case "pacman":
return version.replace(/-/g, "_")
case "rpm":
case "deb":
return version.replace(/-/g, "~")
default:
return version
}
}
async writeDesktopEntry(targetSpecificOptions: LinuxTargetSpecificOptions, exec?: string, destination?: string | null, extra?: Record<string, string>): Promise<string> {
const data = await this.computeDesktopEntry(targetSpecificOptions, exec, extra)
const file = destination || (await this.packager.getTempFile(`${this.packager.appInfo.productFilename}.desktop`))
await outputFile(file, data)
return file
}
computeDesktopEntry(targetSpecificOptions: LinuxTargetSpecificOptions, exec?: string, extra?: Record<string, string>): Promise<string> {
if (exec != null && exec.length === 0) {
throw new Error("Specified exec is empty")
}
if (targetSpecificOptions.desktop?.entry?.Exec) {
throw new Error("Please specify executable name as linux.executableName instead of linux.desktop.Exec")
}
const packager = this.packager
const appInfo = packager.appInfo
const executableArgs = targetSpecificOptions.executableArgs
if (exec == null) {
exec = `${installPrefix}/${appInfo.sanitizedProductName}/${packager.executableName}`
if (!/^[/0-9A-Za-z._-]+$/.test(exec)) {
exec = `"${exec}"`
}
if (executableArgs) {
exec += " "
exec += executableArgs.join(" ")
}
const execCodes = ["%f", "%u", "%F", "%U"]
if (executableArgs == null || executableArgs.findIndex(arg => execCodes.includes(arg)) === -1) {
exec += " %U"
}
}
const desktopMeta: any = {
Name: appInfo.productName,
Exec: exec,
Terminal: "false",
Type: "Application",
Icon: packager.executableName,
StartupWMClass: appInfo.productName,
...extra,
...(targetSpecificOptions.desktop?.entry ?? {}),
}
const description = this.getDescription(targetSpecificOptions)
if (!isEmptyOrSpaces(description)) {
desktopMeta.Comment = description
}
const mimeTypes: Array<string> = asArray(targetSpecificOptions.mimeTypes)
for (const fileAssociation of packager.fileAssociations) {
if (fileAssociation.mimeType != null) {
mimeTypes.push(fileAssociation.mimeType)
}
}
for (const protocol of asArray(packager.config.protocols).concat(asArray(packager.platformSpecificBuildOptions.protocols))) {
for (const scheme of asArray(protocol.schemes)) {
mimeTypes.push(`x-scheme-handler/${scheme}`)
}
}
if (mimeTypes.length !== 0) {
desktopMeta.MimeType = mimeTypes.join(";") + ";"
}
let category = targetSpecificOptions.category
if (isEmptyOrSpaces(category)) {
const macCategory = (packager.config.mac || {}).category
if (macCategory != null) {
category = macToLinuxCategory[macCategory]
}
if (category == null) {
if (macCategory != null) {
log.warn({ macCategory }, "cannot map macOS category to Linux. If possible mapping is known for you, please file issue to add it.")
}
log.warn(
{
reason: "linux.category is not set and cannot map from macOS",
docs: "https://www.electron.build/linux",
},
'application Linux category is set to default "Utility"'
)
category = "Utility"
}
}
desktopMeta.Categories = `${category}${category.endsWith(";") ? "" : ";"}`
let data = `[Desktop Entry]`
for (const name of Object.keys(desktopMeta)) {
data += `\n${name}=${desktopMeta[name]}`
}
data += "\n"
const desktopActions = targetSpecificOptions.desktop?.desktopActions ?? {}
for (const [actionName, config] of Object.entries(desktopActions)) {
if (!Object.keys(config ?? {}).length) {
continue
}
data += `\n[Desktop Action ${actionName}]`
for (const [key, value] of Object.entries(config ?? {})) {
data += `\n${key}=${value}`
}
data += "\n"
}
return Promise.resolve(data)
}
}
const macToLinuxCategory: any = {
"public.app-category.graphics-design": "Graphics",
"public.app-category.developer-tools": "Development",
"public.app-category.education": "Education",
"public.app-category.games": "Game",
"public.app-category.video": "Video;AudioVideo",
"public.app-category.utilities": "Utility",
"public.app-category.social-networking": "Network;Chat",
"public.app-category.finance": "Office;Finance",
"public.app-category.music": "Audio;AudioVideo",
}