diff --git a/electron/scripts/normalize-win32-artifacts.mjs b/electron/scripts/normalize-win32-artifacts.mjs index 789d755..a0a7883 100755 --- a/electron/scripts/normalize-win32-artifacts.mjs +++ b/electron/scripts/normalize-win32-artifacts.mjs @@ -1,7 +1,8 @@ #!/usr/bin/env node -import { promises as fs } from 'fs'; +import { promises as fsPromises, createReadStream } from 'fs'; import path from 'path'; import process from 'process'; +import { createHash } from 'crypto'; const log = (message, data) => { if (data) { @@ -15,13 +16,167 @@ const releaseDir = path.resolve(process.cwd(), 'release'); const exists = async (filePath) => { try { - await fs.access(filePath); + await fsPromises.access(filePath); return true; } catch (_error) { return false; } }; +const computeSha512 = async (filePath) => { + const hash = createHash('sha512'); + await new Promise((resolve, reject) => { + const stream = createReadStream(filePath); + stream.on('data', chunk => hash.update(chunk)); + stream.on('error', reject); + stream.on('end', resolve); + }); + return hash.digest('base64'); +}; + +const toBasename = (value) => { + if (typeof value !== 'string' || value.trim() === '') { + return null; + } + return path.basename(value.trim()); +}; + +const updateManifestFile = async (manifestPath, renameMap, hashMap) => { + const raw = await fsPromises.readFile(manifestPath, 'utf8'); + const lines = raw.split(/\r?\n/); + + let inFilesSection = false; + let inPackagesSection = false; + let currentFileKey = null; + let currentPackageFileKey = null; + let rootFileKey = null; + + const replaceWithRename = (value) => { + const base = toBasename(value); + if (!base) { + return value; + } + const renamed = renameMap.get(base); + if (!renamed || renamed === base) { + return value; + } + return value.endsWith(base) + ? value.slice(0, value.length - base.length) + renamed + : value.replace(base, renamed); + }; + + const applyHashIfAvailable = (existingLine, key, indentPattern) => { + if (!key) { + return existingLine; + } + const hash = hashMap.get(key); + if (!hash) { + return existingLine; + } + return existingLine.replace(indentPattern, `$1${hash}`); + }; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const trimmed = line.trim(); + + if (trimmed === 'files:') { + inFilesSection = true; + inPackagesSection = false; + currentFileKey = null; + continue; + } + if (trimmed === 'packages:') { + inFilesSection = false; + inPackagesSection = true; + currentPackageFileKey = null; + continue; + } + if (!line.startsWith(' ')) { + inFilesSection = trimmed === '' ? inFilesSection : false; + if (trimmed !== 'packages:') { + inPackagesSection = false; + } + } + + if (inFilesSection) { + const urlMatch = line.match(/^\s*-\s+url:\s+(.*)$/); + if (urlMatch) { + const rawValue = urlMatch[1]; + const updatedValue = replaceWithRename(rawValue); + if (updatedValue !== rawValue) { + lines[index] = line.replace(rawValue, updatedValue); + } + currentFileKey = toBasename(updatedValue); + continue; + } + + const pathMatch = line.match(/^\s+path:\s+(.*)$/); + if (pathMatch) { + const rawValue = pathMatch[1]; + const updatedValue = replaceWithRename(rawValue); + if (updatedValue !== rawValue) { + lines[index] = line.replace(rawValue, updatedValue); + } + currentFileKey = toBasename(updatedValue); + continue; + } + + const shaMatch = line.match(/^(\s+sha512:\s+)(.*)$/); + if (shaMatch) { + lines[index] = applyHashIfAvailable(line, currentFileKey, /^(\s+sha512:\s+).*/); + continue; + } + continue; + } + + if (inPackagesSection) { + const packagePathMatch = line.match(/^\s{4}path:\s+(.*)$/); + if (packagePathMatch) { + const rawValue = packagePathMatch[1]; + const updatedValue = replaceWithRename(rawValue); + if (updatedValue !== rawValue) { + lines[index] = line.replace(rawValue, updatedValue); + } + currentPackageFileKey = toBasename(updatedValue); + continue; + } + + const packageShaMatch = line.match(/^(\s{4}sha512:\s+).*/); + if (packageShaMatch) { + lines[index] = applyHashIfAvailable(line, currentPackageFileKey, /^(\s{4}sha512:\s+).*/); + continue; + } + continue; + } + + const rootPathMatch = line.match(/^path:\s+(.*)$/); + if (rootPathMatch) { + const rawValue = rootPathMatch[1]; + const updatedValue = replaceWithRename(rawValue); + if (updatedValue !== rawValue) { + lines[index] = line.replace(rawValue, updatedValue); + } + rootFileKey = toBasename(updatedValue); + continue; + } + + const rootShaMatch = line.match(/^(sha512:\s+).*/); + if (rootShaMatch) { + lines[index] = applyHashIfAvailable(line, rootFileKey, /^(sha512:\s+).*/); + continue; + } + } + + const updatedContent = lines.join('\n'); + if (updatedContent !== raw) { + await fsPromises.writeFile(manifestPath, updatedContent, 'utf8'); + log('Updated manifest metadata.', { manifest: path.basename(manifestPath) }); + } else { + log('Manifest already up to date.', { manifest: path.basename(manifestPath) }); + } +}; + const updateName = (name) => name.replace(/-ia32-/gi, '-win32-'); const main = async () => { @@ -31,14 +186,15 @@ const main = async () => { return; } - const entries = await fs.readdir(releaseDir); + const entries = await fsPromises.readdir(releaseDir); const ia32Executables = entries.filter(name => /-ia32-/i.test(name) && name.endsWith('.exe')); if (ia32Executables.length === 0) { - log('No ia32 Windows executables detected; nothing to normalize.'); - return; + log('No ia32 Windows executables detected; skipping rename but refreshing manifest checksums.'); + } else { + log('Detected ia32 executables that require normalization.', ia32Executables); } - log('Detected ia32 executables that require normalization.', ia32Executables); + const renameMap = new Map(); for (const fileName of ia32Executables) { const sourcePath = path.join(releaseDir, fileName); @@ -47,8 +203,9 @@ const main = async () => { continue; } const targetPath = path.join(releaseDir, targetName); - await fs.rename(sourcePath, targetPath); + await fsPromises.rename(sourcePath, targetPath); log('Renamed executable.', { from: fileName, to: targetName }); + renameMap.set(fileName, targetName); } const blockmaps = entries.filter(name => /-ia32-/i.test(name) && name.endsWith('.exe.blockmap')); @@ -59,30 +216,46 @@ const main = async () => { continue; } const targetPath = path.join(releaseDir, targetName); - await fs.rename(sourcePath, targetPath); + await fsPromises.rename(sourcePath, targetPath); log('Renamed blockmap.', { from: blockmap, to: targetName }); + renameMap.set(blockmap, targetName); } const manifestPath = path.join(releaseDir, 'latest.yml'); + const fallbackManifest = path.join(releaseDir, 'latest-win32.yml'); const hasManifest = await exists(manifestPath); - if (!hasManifest) { - const fallbackManifest = path.join(releaseDir, 'latest-win32.yml'); - if (await exists(fallbackManifest)) { - log('Found existing latest-win32.yml manifest; normalization already applied.'); - return; + const hasFallback = await exists(fallbackManifest); + + if (!hasFallback && hasManifest) { + await fsPromises.copyFile(manifestPath, fallbackManifest); + log('Seeded latest-win32.yml from latest.yml for normalization.'); + } + + const hashes = new Map(); + const updatedEntries = await fsPromises.readdir(releaseDir); + for (const entry of updatedEntries) { + if (/\.exe(\.blockmap)?$/i.test(entry)) { + const fullPath = path.join(releaseDir, entry); + hashes.set(entry, await computeSha512(fullPath)); } - log('No latest.yml manifest found for ia32 build; cannot update metadata.'); - return; } - const manifestRaw = await fs.readFile(manifestPath, 'utf8'); - const normalizedManifest = manifestRaw.replace(/-ia32-/gi, '-win32-'); - const targetManifestPath = path.join(releaseDir, 'latest-win32.yml'); - await fs.writeFile(targetManifestPath, normalizedManifest, 'utf8'); - log('Wrote normalized manifest.', { path: path.relative(process.cwd(), targetManifestPath) }); + const manifestsToUpdate = []; + if (await exists(manifestPath)) { + manifestsToUpdate.push(manifestPath); + } + if (await exists(fallbackManifest)) { + manifestsToUpdate.push(fallbackManifest); + } + + if (manifestsToUpdate.length === 0) { + log('No manifest files found to update; please verify the release output.'); + return; + } - await fs.unlink(manifestPath); - log('Removed original latest.yml manifest to prevent mismatched metadata.'); + for (const manifest of manifestsToUpdate) { + await updateManifestFile(manifest, renameMap, hashes); + } }; main().catch(error => {