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

✨ Set icons and metadata to Windows executables #268

Merged
merged 5 commits into from
Aug 19, 2024
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
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
};
Loading