Skip to content

Commit

Permalink
✨ Set icons and metadata to Windows executables (#268)
Browse files Browse the repository at this point in the history
* ✨ Set icons and metadata to Windows executables

* ⚡ Update the code to support Node.js v12+ and fix the code when only an icon in modes.window.icon was specified
  • Loading branch information
CosmoMyzrailGorynych authored Aug 19, 2024
1 parent 4987353 commit a1e0237
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 0 deletions.
Binary file added images/defaultIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
"edit-json-file": "^1.6.2",
"follow-redirects": "^1.13.1",
"fs-extra": "^9.0.1",
"png2icons": "^2.0.1",
"recursive-readdir": "^2.2.2",
"resedit": "^2.0.2",
"spawn-command": "^0.0.2-1",
"tcp-port-used": "^1.0.2",
"uuid": "^8.3.2",
Expand Down
18 changes: 18 additions & 0 deletions src/modules/bundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const constants = require('../constants');
const frontendlib = require('./frontendlib');
const hostproject = require('./hostproject');
const utils = require('../utils');
const {patchWindowsExecutable} = require('./exepatch');
const path = require('path');

async function createAsarFile() {
Expand Down Expand Up @@ -87,6 +88,23 @@ module.exports.bundleApp = async (isRelease, copyStorage) => {
}
}

utils.log('Patching windows executables...');
try {
await Promise.all(Object.keys(constants.files.binaries.win32).map(async (arch) => {
const origBinaryName = constants.files.binaries.win32[arch];
const filepath = hostproject.hasHostProject() ? `bin/${origBinaryName}` : origBinaryName.replace('neutralino', binaryName);
const winPath = `${buildDir}/${binaryName}/${filepath}`;
if (await fse.exists(winPath)) {
await patchWindowsExecutable(winPath);
}
}))
}
catch (err) {
console.error(err);
utils.error('Could not patch windows executable');
process.exit(1);
}

for (let dependency of constants.files.dependencies) {
fse.copySync(`bin/${dependency}`, `${buildDir}/${binaryName}/${dependency}`);
}
Expand Down
125 changes: 125 additions & 0 deletions src/modules/exepatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const fse = require('fs-extra');
const png2icons = require('png2icons');
const path = require('path');

const config = require('./config');
// 1033 means "English (United States)" locale in Windows
const EN_US = 1033;

// works like `a ?? b ?? c ?? ...`
const pickNotNullish = (...values) => {
for (let i = 0; i < values.length - 1; i++) {
if (values[i] !== undefined && values[i] !== null) {
return values[i];
}
}
return values[values.length - 1];
}

const getWindowsMetadata = (config) => {
const copyright = pickNotNullish(
config.copyright,
`Copyright © ${new Date().getFullYear()} ${pickNotNullish(config.author, 'All rights reserved.')}`
);
const versionStrings = {
CompanyName: config.author,
FileDescription: pickNotNullish(config.description, 'A Neutralinojs application'),
ProductVersion: config.version,
LegalCopyright: copyright,
InternalName: config.cli.binaryName,
OriginalFilename: config.cli.binaryName,
ProductName: pickNotNullish(config.applicationName, config.cli.binaryName),
SpecialBuild: config.cli.binaryName,
};
// Strip away any undefined values from the versionStrings object
// in case some configuration options are missing.
Object.keys(versionStrings).forEach((option) => {
if (versionStrings[option] === undefined) {
delete versionStrings[option];
}
});
return versionStrings;
};

/**
* Reads the specified png file and returns a buffer of a converted ICO file.
*/
const convertPngToIco = async (src) => {
const icon = await fse.readFile(src);
return png2icons.createICO(icon, png2icons.HERMITE, 0, true, true);
}

/**
* Edits the Windows executable with an icon
* and changes its metadata to include the app's name and version.
*
* @param {string} src The path to the .exe file
*/
const patchWindowsExecutable = async (src) => {
const resedit = await import('resedit');
// pe-library is a direct dependency of resedit
const peLibrary = await import('pe-library');
const configObj = config.get();
// Create an executable object representation of the .exe file and get its resource object
const exe = peLibrary.NtExecutable.from(await fse.readFile(src));
const res = peLibrary.NtExecutableResource.from(exe);

// If an icon was not set for the project, try to get the icon from the window's config
let windowIcon = null;
if (configObj.modes && configObj.modes.window && configObj.modes.window.icon) {
windowIcon = configObj.modes.window.icon;
// Do not use non-PNG window icons.
if (windowIcon.split('.').pop().toLowerCase() !== 'png') {
windowIcon = null;
} else {
// Remove the leading slash if it exists, we need a path relative to CWD.
if (windowIcon[0] === '/') {
windowIcon = windowIcon.slice(1);
}
// Get the path from the resources directory relative to CWD.
windowIcon = path.join(process.cwd(), windowIcon);
}
}

const iconPath = pickNotNullish(
configObj.applicationIcon,
windowIcon,
`${__dirname}/../../images/defaultIcon.png` // Default to a neutralinojs icon
);
const icoBuffer = await convertPngToIco(iconPath);
// Create an icon file that contains the icon
const iconFile = resedit.Data.IconFile.from(icoBuffer);
// Put the group into the resource
resedit.Resource.IconGroupEntry.replaceIconsForResource(
res.entries,
// 0 is the default icon group
0,
EN_US,
iconFile.icons.map(i => i.data)
);

// Fill in version info
const vi = resedit.Resource.VersionInfo.createEmpty();
const [major, minor, patch] = pickNotNullish(configObj.version, '1.0.0').split(".");
vi.setFileVersion(
pickNotNullish(major, 1), // Version number
pickNotNullish(minor, 0),
pickNotNullish(patch, 0),
0, // Revision number, isn't used in most JS applications so use a 0.
EN_US
);
vi.setStringValues({
lang: EN_US,
codepage: 1200
}, getWindowsMetadata(configObj));
vi.outputToResourceEntries(res.entries);
// Write the modified resource to the executable object
res.outputResource(exe);
// Output the modified executable to the original file path
const outBuffer = Buffer.from(exe.generate());
await fse.writeFile(src, outBuffer);
}

module.exports = {
patchWindowsExecutable
};

0 comments on commit a1e0237

Please sign in to comment.