Files
web2app/.github/scripts/generate-app-icons.mjs
Cursor Agent 0e6e546f44 fix(ci): center-crop non-square logos before tauri icon generation
tauri icon requires a square source image. Uploaded logos like 727x721
caused Prepare template to fail. Use sharp to center-crop to square
before generating platform icons.

Co-authored-by: ShuMengya <shumengya666@outlook.com>
2026-05-30 08:15:25 +00:00

114 lines
3.1 KiB
JavaScript

import { execSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import sharp from "sharp";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, "../..");
const distDir = path.join(root, "template", "dist");
const templateDir = path.join(root, "template");
const ICON_PRIORITY = ["logo.png", "logo.jpg", "logo.jpeg", "favicon.ico"];
const BUNDLE_ICONS = [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico",
];
function findIconSource(baseDir) {
if (!fs.existsSync(baseDir)) return null;
for (const name of ICON_PRIORITY) {
const direct = path.join(baseDir, name);
if (fs.existsSync(direct)) return direct;
}
for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
for (const name of ICON_PRIORITY) {
const nested = path.join(baseDir, entry.name, name);
if (fs.existsSync(nested)) return nested;
}
}
return null;
}
export function findIconSourceForJob(rootDir, jobId, dist) {
const buildDir = jobId ? path.join(rootDir, "builds", jobId) : null;
if (buildDir && fs.existsSync(buildDir)) {
const fromUpload = findIconSource(buildDir);
if (fromUpload) return fromUpload;
}
return findIconSource(dist);
}
async function ensureSquareIconSource(iconSource) {
const meta = await sharp(iconSource).metadata();
const width = meta.width ?? 0;
const height = meta.height ?? 0;
if (!width || !height) {
throw new Error(`Unable to read icon dimensions: ${iconSource}`);
}
if (width === height) {
return iconSource;
}
const size = Math.min(width, height);
const left = Math.floor((width - size) / 2);
const top = Math.floor((height - size) / 2);
const squarePath = path.join(
os.tmpdir(),
`web2app-icon-${path.basename(iconSource, path.extname(iconSource))}.png`,
);
await sharp(iconSource)
.extract({ left, top, width: size, height: size })
.png()
.toFile(squarePath);
console.log(
`Normalized icon to square by center crop (${width}x${height} -> ${size}x${size})`,
);
return squarePath;
}
export async function generateAppIcons(options = {}) {
const { jobId } = options;
if (!fs.existsSync(distDir)) {
console.log("No dist directory, keeping default icons");
return false;
}
const iconSource = findIconSourceForJob(root, jobId, distDir);
if (!iconSource) {
console.log("No logo.png or favicon.ico found, keeping default icons");
return false;
}
const squareSource = await ensureSquareIconSource(iconSource);
console.log(`Generating app icons from: ${path.relative(root, iconSource)}`);
execSync(`npx tauri icon "${squareSource}"`, {
cwd: templateDir,
stdio: "inherit",
});
return true;
}
export { BUNDLE_ICONS };
const isCli =
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isCli) {
await generateAppIcons({ jobId: process.env.JOB_ID });
}