diff --git a/.gitignore b/.gitignore index 033cbf9..ac3b077 100644 --- a/.gitignore +++ b/.gitignore @@ -10,14 +10,6 @@ coverage/ dist/ dist-electron/ release/ -build/ -!build/ -build/* -!build/ICON_FILES_REQUIRED.txt -!build/icon-source.png -!build/icon.png -!build/icon.ico -!build/icon.icns # Environment variables .env diff --git a/build/budget-file-icon.icns b/build/budget-file-icon.icns new file mode 100644 index 0000000..560d305 Binary files /dev/null and b/build/budget-file-icon.icns differ diff --git a/build/budget-file-icon.ico b/build/budget-file-icon.ico new file mode 100644 index 0000000..216066c Binary files /dev/null and b/build/budget-file-icon.ico differ diff --git a/build/budget-file-icon.png b/build/budget-file-icon.png new file mode 100644 index 0000000..521899a Binary files /dev/null and b/build/budget-file-icon.png differ diff --git a/build/budget-icon-source.png b/build/budget-icon-source.png new file mode 100644 index 0000000..521899a Binary files /dev/null and b/build/budget-icon-source.png differ diff --git a/build/entitlements.mac.inherit.plist b/build/entitlements.mac.inherit.plist new file mode 100644 index 0000000..48f7bf5 --- /dev/null +++ b/build/entitlements.mac.inherit.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist new file mode 100644 index 0000000..48f7bf5 --- /dev/null +++ b/build/entitlements.mac.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/scripts/generate-icons.js b/scripts/generate-icons.js index 7116ec2..92ab2ca 100644 --- a/scripts/generate-icons.js +++ b/scripts/generate-icons.js @@ -10,7 +10,9 @@ const rootDir = path.resolve(__dirname, '..'); const buildDir = path.join(rootDir, 'build'); const sourcePng = path.join(buildDir, 'icon-source.png'); +const sourceWinPng = path.join(buildDir, 'icon-source-win.png'); const budgetSourcePng = path.join(buildDir, 'budget-icon-source.png'); +const budgetSourceWinPng = path.join(buildDir, 'budget-icon-source-win.png'); const skipIfMissing = process.argv.includes('--if-present'); function fail(message) { @@ -18,7 +20,65 @@ function fail(message) { process.exit(1); } -function generateIconSet(inputBuffer, outputBaseName) { +function getPngDimensions(buffer, label) { + if (!buffer || buffer.length < 24) { + fail(`Invalid PNG data for ${label}: file is too small.`); + } + + // PNG signature (8 bytes) + const signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; + const hasValidSignature = signature.every((value, index) => buffer[index] === value); + if (!hasValidSignature) { + fail(`Invalid PNG data for ${label}: missing PNG signature.`); + } + + const width = buffer.readUInt32BE(16); + const height = buffer.readUInt32BE(20); + + if (!width || !height) { + fail(`Invalid PNG dimensions for ${label}: width=${width}, height=${height}.`); + } + + return { width, height }; +} + +function assertSquareIcon(buffer, label) { + const { width, height } = getPngDimensions(buffer, label); + if (width !== height) { + fail( + [ + `${label} must be a square PNG for reliable .ico/.icns output.`, + `Found ${width}x${height}.`, + 'Please export a square image (recommended 1024x1024) and try again.', + ].join(' ') + ); + } + + if (width < 256) { + fail( + [ + `${label} is too small (${width}x${height}).`, + 'Use at least 256x256 (recommended 1024x1024) for crisp icon variants.', + ].join(' ') + ); + } + + return { width, height }; +} + +function readValidatedSquarePng(filePath, label) { + const buffer = fs.readFileSync(filePath); + + if (!buffer || buffer.length === 0) { + fail(`Source icon file is empty: ${label}`); + } + + assertSquareIcon(buffer, label); + return buffer; +} + +function generateIconSet(inputBuffer, outputBaseName, options = {}) { + const { icoInputBuffer = inputBuffer, sourceFilePath, icoSourceLabel } = options; const outputPng = path.join(buildDir, `${outputBaseName}.png`); const outputIco = path.join(buildDir, `${outputBaseName}.ico`); const outputIcns = path.join(buildDir, `${outputBaseName}.icns`); @@ -28,19 +88,21 @@ function generateIconSet(inputBuffer, outputBaseName) { fail(`Failed to generate ICNS file for ${outputBaseName}`); } - const icoBuffer = png2icons.createICO(inputBuffer, png2icons.BICUBIC2, 0, false, true); + // Use BMP payloads for all ICO entries to maximize compatibility across + // shell previews and older Windows icon decoders. + const icoBuffer = png2icons.createICO(icoInputBuffer, png2icons.BICUBIC2, 0, false, false); if (!icoBuffer) { fail(`Failed to generate ICO file for ${outputBaseName}`); } - fs.copyFileSync( - outputBaseName === 'icon' ? sourcePng : budgetSourcePng, - outputPng - ); + fs.copyFileSync(sourceFilePath, outputPng); fs.writeFileSync(outputIcns, icnsBuffer); fs.writeFileSync(outputIco, icoBuffer); console.log(`✓ Generated icon files for ${outputBaseName}:`); + if (icoSourceLabel) { + console.log(` - Windows ICO source: ${icoSourceLabel}`); + } console.log(` - ${path.relative(rootDir, outputPng)}`); console.log(` - ${path.relative(rootDir, outputIcns)}`); console.log(` - ${path.relative(rootDir, outputIco)}`); @@ -60,24 +122,48 @@ if (!fs.existsSync(sourcePng)) { ); } -const inputBuffer = fs.readFileSync(sourcePng); - -if (!inputBuffer || inputBuffer.length === 0) { - fail('Source icon file is empty: build/icon-source.png'); -} +const inputBuffer = readValidatedSquarePng(sourcePng, 'build/icon-source.png'); +const inputWinBuffer = fs.existsSync(sourceWinPng) + ? readValidatedSquarePng(sourceWinPng, 'build/icon-source-win.png') + : inputBuffer; fs.mkdirSync(buildDir, { recursive: true }); -generateIconSet(inputBuffer, 'icon'); +generateIconSet(inputBuffer, 'icon', { + icoInputBuffer: inputWinBuffer, + sourceFilePath: sourcePng, + icoSourceLabel: fs.existsSync(sourceWinPng) ? 'build/icon-source-win.png' : 'build/icon-source.png', +}); if (fs.existsSync(budgetSourcePng)) { - const budgetInputBuffer = fs.readFileSync(budgetSourcePng); - - if (!budgetInputBuffer || budgetInputBuffer.length === 0) { - fail('Source icon file is empty: build/budget-icon-source.png'); + const budgetInputBuffer = readValidatedSquarePng(budgetSourcePng, 'build/budget-icon-source.png'); + const budgetWinInputBuffer = fs.existsSync(budgetSourceWinPng) + ? readValidatedSquarePng(budgetSourceWinPng, 'build/budget-icon-source-win.png') + : budgetInputBuffer; + + const { width, height } = getPngDimensions(budgetInputBuffer, 'build/budget-icon-source.png'); + + if (width !== height) { + console.warn( + [ + '⚠️ build/budget-icon-source.png is not square', + `(${width}x${height}).`, + 'Using app icon for .budget file associations to avoid corrupted icon output.', + ].join(' ') + ); + + fs.copyFileSync(path.join(buildDir, 'icon.png'), path.join(buildDir, 'budget-file-icon.png')); + fs.copyFileSync(path.join(buildDir, 'icon.icns'), path.join(buildDir, 'budget-file-icon.icns')); + fs.copyFileSync(path.join(buildDir, 'icon.ico'), path.join(buildDir, 'budget-file-icon.ico')); + } else { + generateIconSet(budgetInputBuffer, 'budget-file-icon', { + icoInputBuffer: budgetWinInputBuffer, + sourceFilePath: budgetSourcePng, + icoSourceLabel: fs.existsSync(budgetSourceWinPng) + ? 'build/budget-icon-source-win.png' + : 'build/budget-icon-source.png', + }); } - - generateIconSet(budgetInputBuffer, 'budget-file-icon'); } else { // Fallback so file association always has a valid icon set. fs.copyFileSync(path.join(buildDir, 'icon.png'), path.join(buildDir, 'budget-file-icon.png'));