Add installer icons for Windows and macOS
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced a new installer icon in ICO format and a complete iconset for macOS, enhancing the visual branding of the installer. - Updated Windows installer scripts to reference the new icon files, ensuring they are included in the installation package. - Implemented a script to prepare the installer icons from a source image, streamlining the icon generation process for different platforms.
BIN
assets/installer.ico
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
assets/installer.iconset/icon_128x128.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
assets/installer.iconset/icon_128x128@2x.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/installer.iconset/icon_16x16.png
Normal file
|
After Width: | Height: | Size: 499 B |
BIN
assets/installer.iconset/icon_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/installer.iconset/icon_256x256.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/installer.iconset/icon_256x256@2x.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
assets/installer.iconset/icon_32x32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/installer.iconset/icon_32x32@2x.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
assets/installer.iconset/icon_512x512.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
assets/installer.iconset/icon_512x512@2x.png
Normal file
|
After Width: | Height: | Size: 371 KiB |
@ -20,6 +20,9 @@
|
|||||||
DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
|
DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
|
||||||
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
|
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
|
||||||
|
|
||||||
|
<Icon Id="InstallerIcon" SourceFile="__INSTALLER_ICON__" />
|
||||||
|
<Property Id="ARPPRODUCTICON" Value="InstallerIcon" />
|
||||||
|
|
||||||
<Property Id="DISABLEADVTSHORTCUTS" Value="1" />
|
<Property Id="DISABLEADVTSHORTCUTS" Value="1" />
|
||||||
<Property Id="MSIINSTALLPERUSER" Value="1" />
|
<Property Id="MSIINSTALLPERUSER" Value="1" />
|
||||||
|
|
||||||
|
|||||||
@ -89,12 +89,21 @@ if ($outputDir -and -not (Test-Path $outputDir)) {
|
|||||||
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$installerIconPath = Join-Path $rootDir "assets\installer.ico"
|
||||||
|
if (-not (Test-Path $installerIconPath)) {
|
||||||
|
throw "Installer icon not found at $installerIconPath. Run scripts/prepare-installer-icons.mjs first."
|
||||||
|
}
|
||||||
|
|
||||||
|
$installerIconWorkPath = Join-Path $workDir "installer.ico"
|
||||||
|
Copy-Item -LiteralPath $installerIconPath -Destination $installerIconWorkPath -Force
|
||||||
|
|
||||||
$msiVersion = Get-MsiVersion $Version
|
$msiVersion = Get-MsiVersion $Version
|
||||||
$wxsContent = Get-Content -LiteralPath $templatePath -Raw
|
$wxsContent = Get-Content -LiteralPath $templatePath -Raw
|
||||||
$wxsContent = $wxsContent.Replace("__PRODUCT_ID__", $ProductId)
|
$wxsContent = $wxsContent.Replace("__PRODUCT_ID__", $ProductId)
|
||||||
$wxsContent = $wxsContent.Replace("__UPGRADE_CODE__", $UpgradeCode)
|
$wxsContent = $wxsContent.Replace("__UPGRADE_CODE__", $UpgradeCode)
|
||||||
$wxsContent = $wxsContent.Replace("__VERSION__", $msiVersion)
|
$wxsContent = $wxsContent.Replace("__VERSION__", $msiVersion)
|
||||||
$wxsContent = $wxsContent.Replace("__SETUP_EXE__", (Escape-WixSourcePath $setupExePath))
|
$wxsContent = $wxsContent.Replace("__SETUP_EXE__", (Escape-WixSourcePath $setupExePath))
|
||||||
|
$wxsContent = $wxsContent.Replace("__INSTALLER_ICON__", (Escape-WixSourcePath $installerIconWorkPath))
|
||||||
|
|
||||||
$projectWxs = Join-Path $workDir "project.wxs"
|
$projectWxs = Join-Path $workDir "project.wxs"
|
||||||
Set-Content -LiteralPath $projectWxs -Value $wxsContent -Encoding UTF8
|
Set-Content -LiteralPath $projectWxs -Value $wxsContent -Encoding UTF8
|
||||||
|
|||||||
@ -81,10 +81,10 @@ if (-not (Test-Path $requiredDeeplinkScript)) {
|
|||||||
Copy-Item -LiteralPath $nsiPath -Destination (Join-Path $workDir "farmcontrol.nsi")
|
Copy-Item -LiteralPath $nsiPath -Destination (Join-Path $workDir "farmcontrol.nsi")
|
||||||
Copy-Item -LiteralPath $installerInclude -Destination (Join-Path $workDir "installer.nsh")
|
Copy-Item -LiteralPath $installerInclude -Destination (Join-Path $workDir "installer.nsh")
|
||||||
|
|
||||||
$iconPath = Join-Path $rootDir "assets\icon.ico"
|
$iconPath = Join-Path $rootDir "assets\installer.ico"
|
||||||
if (Test-Path $iconPath) {
|
if (Test-Path $iconPath) {
|
||||||
Copy-Item -LiteralPath $iconPath -Destination (Join-Path $workDir "icon.ico") -Force
|
Copy-Item -LiteralPath $iconPath -Destination (Join-Path $workDir "icon.ico") -Force
|
||||||
Write-Host "Using custom installer icon from $iconPath"
|
Write-Host "Using installer icon from $iconPath"
|
||||||
}
|
}
|
||||||
|
|
||||||
$makensis = Find-Makensis
|
$makensis = Find-Makensis
|
||||||
|
|||||||
@ -189,6 +189,7 @@ function cleanStagingArtifacts(keepNames) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MAC_DMG_ASSETS_DIR = path.join(rootDir, 'packaging/macos/dmg')
|
const MAC_DMG_ASSETS_DIR = path.join(rootDir, 'packaging/macos/dmg')
|
||||||
|
const MAC_INSTALLER_ICONSET_PATH = path.join(rootDir, 'assets/installer.iconset')
|
||||||
const MAC_DMG_BACKGROUND_PATH = path.join(rootDir, 'assets/dmg/background.png')
|
const MAC_DMG_BACKGROUND_PATH = path.join(rootDir, 'assets/dmg/background.png')
|
||||||
const MAC_DMG_BACKGROUND_RETINA_PATH = path.join(
|
const MAC_DMG_BACKGROUND_RETINA_PATH = path.join(
|
||||||
rootDir,
|
rootDir,
|
||||||
@ -198,25 +199,85 @@ const MAC_DMG_APP_NAME = 'Farm Control.app'
|
|||||||
const MAC_DMG_WINDOW_WIDTH = 540
|
const MAC_DMG_WINDOW_WIDTH = 540
|
||||||
const MAC_DMG_WINDOW_HEIGHT = 380
|
const MAC_DMG_WINDOW_HEIGHT = 380
|
||||||
|
|
||||||
|
function findCommand(command) {
|
||||||
|
const which = spawnSync('which', [command], { encoding: 'utf8' })
|
||||||
|
if (which.status === 0 && which.stdout.trim()) {
|
||||||
|
return which.stdout.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInstallerIcns(destinationPath) {
|
||||||
|
if (!existsSync(MAC_INSTALLER_ICONSET_PATH)) {
|
||||||
|
throw new Error(
|
||||||
|
`Installer iconset not found at ${MAC_INSTALLER_ICONSET_PATH}. Run scripts/prepare-installer-icons.mjs first.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(path.dirname(destinationPath), { recursive: true })
|
||||||
|
rmSync(destinationPath, { force: true })
|
||||||
|
|
||||||
|
const iconutil = spawnSync(
|
||||||
|
'iconutil',
|
||||||
|
['-c', 'icns', '-o', destinationPath, MAC_INSTALLER_ICONSET_PATH],
|
||||||
|
{ stdio: 'inherit' }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (iconutil.status !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`iconutil failed to create installer icns with exit code ${iconutil.status ?? 1}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return destinationPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMacInstallerFileIcon(targetPath, icnsPath) {
|
||||||
|
if (!existsSync(icnsPath)) {
|
||||||
|
console.warn(
|
||||||
|
`finalize-desktop-artifacts: installer icns not found at ${icnsPath}, skipping custom icon`
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileicon = findCommand('fileicon')
|
||||||
|
if (fileicon) {
|
||||||
|
const result = spawnSync(fileicon, ['set', targetPath, icnsPath], {
|
||||||
|
stdio: 'inherit'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.status === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapedTarget = targetPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
||||||
|
const escapedIcon = icnsPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
||||||
|
const result = spawnSync(
|
||||||
|
'osascript',
|
||||||
|
[
|
||||||
|
'-e',
|
||||||
|
`tell application "Finder" to set icon of (POSIX file "${escapedTarget}") to (read (POSIX file "${escapedIcon}") as picture)`
|
||||||
|
],
|
||||||
|
{ stdio: 'inherit' }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.status === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
'finalize-desktop-artifacts: could not apply installer icon; install fileicon or run on macOS with Finder access'
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
function ensureMacDmgAssets() {
|
function ensureMacDmgAssets() {
|
||||||
mkdirSync(MAC_DMG_ASSETS_DIR, { recursive: true })
|
mkdirSync(MAC_DMG_ASSETS_DIR, { recursive: true })
|
||||||
|
|
||||||
const voliconPath = path.join(MAC_DMG_ASSETS_DIR, 'volicon.icns')
|
const voliconPath = path.join(MAC_DMG_ASSETS_DIR, 'volicon.icns')
|
||||||
const iconsetPath = path.join(rootDir, 'assets/icon.iconset')
|
buildInstallerIcns(voliconPath)
|
||||||
|
|
||||||
if (!existsSync(voliconPath) && existsSync(iconsetPath)) {
|
|
||||||
const iconutil = spawnSync(
|
|
||||||
'iconutil',
|
|
||||||
['-c', 'icns', '-o', voliconPath, iconsetPath],
|
|
||||||
{ stdio: 'inherit' }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (iconutil.status !== 0) {
|
|
||||||
throw new Error(
|
|
||||||
`iconutil failed to create DMG volicon with exit code ${iconutil.status ?? 1}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(MAC_DMG_BACKGROUND_PATH)) {
|
if (!existsSync(MAC_DMG_BACKGROUND_PATH)) {
|
||||||
throw new Error(`DMG background not found at ${MAC_DMG_BACKGROUND_PATH}`)
|
throw new Error(`DMG background not found at ${MAC_DMG_BACKGROUND_PATH}`)
|
||||||
@ -229,7 +290,7 @@ function ensureMacDmgAssets() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
volicon: existsSync(voliconPath) ? voliconPath : null,
|
volicon: voliconPath,
|
||||||
background: MAC_DMG_BACKGROUND_PATH
|
background: MAC_DMG_BACKGROUND_PATH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -381,6 +442,7 @@ async function buildMacDmg(appBundlePath, arch) {
|
|||||||
appBundleName,
|
appBundleName,
|
||||||
dmgAssets
|
dmgAssets
|
||||||
)
|
)
|
||||||
|
applyMacInstallerFileIcon(builtDmgPath, dmgAssets.volicon)
|
||||||
} finally {
|
} finally {
|
||||||
if (!useDirectSource) {
|
if (!useDirectSource) {
|
||||||
rmSync(stagingDir, { recursive: true, force: true })
|
rmSync(stagingDir, { recursive: true, force: true })
|
||||||
@ -425,6 +487,12 @@ function buildMacPkg(appBundlePath, arch) {
|
|||||||
throw new Error(`pkgbuild failed with exit code ${result.status ?? 1}`)
|
throw new Error(`pkgbuild failed with exit code ${result.status ?? 1}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const installerIcnsPath = path.join(MAC_DMG_ASSETS_DIR, 'volicon.icns')
|
||||||
|
if (!existsSync(installerIcnsPath)) {
|
||||||
|
buildInstallerIcns(installerIcnsPath)
|
||||||
|
}
|
||||||
|
applyMacInstallerFileIcon(pkgPath, installerIcnsPath)
|
||||||
|
|
||||||
console.log(`Published ${pkgPath}`)
|
console.log(`Published ${pkgPath}`)
|
||||||
return pkgPath
|
return pkgPath
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,6 +74,16 @@ if (prepareIcons.status !== 0) {
|
|||||||
process.exit(prepareIcons.status ?? 1);
|
process.exit(prepareIcons.status ?? 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prepareInstallerIcons = spawnSync(
|
||||||
|
"bun",
|
||||||
|
[path.join(rootDir, "scripts/prepare-installer-icons.mjs")],
|
||||||
|
{ cwd: rootDir, stdio: "inherit", env: process.env },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (prepareInstallerIcons.status !== 0) {
|
||||||
|
process.exit(prepareInstallerIcons.status ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
const windowsIconPath = path.join(rootDir, "assets/icon.ico");
|
const windowsIconPath = path.join(rootDir, "assets/icon.ico");
|
||||||
if (!existsSync(windowsIconPath)) {
|
if (!existsSync(windowsIconPath)) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|||||||
86
scripts/prepare-installer-icons.mjs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { existsSync, mkdirSync, rmSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { Jimp } from 'jimp'
|
||||||
|
import pngToIco from 'png-to-ico'
|
||||||
|
|
||||||
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
const INSTALLER_SOURCE = path.join(
|
||||||
|
rootDir,
|
||||||
|
'assets/logos/farmcontrolinstaller.png'
|
||||||
|
)
|
||||||
|
|
||||||
|
const MAC_ICONSET_ENTRIES = [
|
||||||
|
{ name: 'icon_16x16.png', size: 16 },
|
||||||
|
{ name: 'icon_16x16@2x.png', size: 32 },
|
||||||
|
{ name: 'icon_32x32.png', size: 32 },
|
||||||
|
{ name: 'icon_32x32@2x.png', size: 64 },
|
||||||
|
{ name: 'icon_128x128.png', size: 128 },
|
||||||
|
{ name: 'icon_128x128@2x.png', size: 256 },
|
||||||
|
{ name: 'icon_256x256.png', size: 256 },
|
||||||
|
{ name: 'icon_256x256@2x.png', size: 512 },
|
||||||
|
{ name: 'icon_512x512.png', size: 512 },
|
||||||
|
{ name: 'icon_512x512@2x.png', size: 1024 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const WIN_ICO_SIZES = [16, 32, 48, 256]
|
||||||
|
|
||||||
|
async function resizePng(sourceImage, size) {
|
||||||
|
return sourceImage
|
||||||
|
.clone()
|
||||||
|
.resize({ w: size, h: size })
|
||||||
|
.getBuffer('image/png')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writePng(filePath, sourceImage, size) {
|
||||||
|
const png = await resizePng(sourceImage, size)
|
||||||
|
await Bun.write(filePath, png)
|
||||||
|
return png
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (!existsSync(INSTALLER_SOURCE)) {
|
||||||
|
console.error(
|
||||||
|
`prepare-installer-icons: source image not found: ${INSTALLER_SOURCE}`
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceImage = await Jimp.read(INSTALLER_SOURCE)
|
||||||
|
if (sourceImage.width < 256 || sourceImage.height < 256) {
|
||||||
|
console.warn(
|
||||||
|
`prepare-installer-icons: source image is ${sourceImage.width}x${sourceImage.height}; recommend at least 256x256`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetsDir = path.join(rootDir, 'assets')
|
||||||
|
mkdirSync(assetsDir, { recursive: true })
|
||||||
|
|
||||||
|
const iconsetDir = path.join(assetsDir, 'installer.iconset')
|
||||||
|
rmSync(iconsetDir, { recursive: true, force: true })
|
||||||
|
mkdirSync(iconsetDir, { recursive: true })
|
||||||
|
|
||||||
|
for (const entry of MAC_ICONSET_ENTRIES) {
|
||||||
|
await writePng(
|
||||||
|
path.join(iconsetDir, entry.name),
|
||||||
|
sourceImage,
|
||||||
|
entry.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const icoPngs = []
|
||||||
|
for (const size of WIN_ICO_SIZES) {
|
||||||
|
icoPngs.push(await resizePng(sourceImage, size))
|
||||||
|
}
|
||||||
|
|
||||||
|
const icoPath = path.join(assetsDir, 'installer.ico')
|
||||||
|
await Bun.write(icoPath, await pngToIco(icoPngs))
|
||||||
|
|
||||||
|
console.log(`prepare-installer-icons: wrote ${iconsetDir}`)
|
||||||
|
console.log(`prepare-installer-icons: wrote ${icoPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`prepare-installer-icons: ${error.message}`)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||