11#!/usr/bin/env node
2- import { promises as fs } from 'fs' ;
2+ import { promises as fsPromises , createReadStream } from 'fs' ;
33import path from 'path' ;
44import process from 'process' ;
5+ import { createHash } from 'crypto' ;
56
67const log = ( message , data ) => {
78 if ( data ) {
@@ -15,13 +16,167 @@ const releaseDir = path.resolve(process.cwd(), 'release');
1516
1617const exists = async ( filePath ) => {
1718 try {
18- await fs . access ( filePath ) ;
19+ await fsPromises . access ( filePath ) ;
1920 return true ;
2021 } catch ( _error ) {
2122 return false ;
2223 }
2324} ;
2425
26+ const computeSha512 = async ( filePath ) => {
27+ const hash = createHash ( 'sha512' ) ;
28+ await new Promise ( ( resolve , reject ) => {
29+ const stream = createReadStream ( filePath ) ;
30+ stream . on ( 'data' , chunk => hash . update ( chunk ) ) ;
31+ stream . on ( 'error' , reject ) ;
32+ stream . on ( 'end' , resolve ) ;
33+ } ) ;
34+ return hash . digest ( 'base64' ) ;
35+ } ;
36+
37+ const toBasename = ( value ) => {
38+ if ( typeof value !== 'string' || value . trim ( ) === '' ) {
39+ return null ;
40+ }
41+ return path . basename ( value . trim ( ) ) ;
42+ } ;
43+
44+ const updateManifestFile = async ( manifestPath , renameMap , hashMap ) => {
45+ const raw = await fsPromises . readFile ( manifestPath , 'utf8' ) ;
46+ const lines = raw . split ( / \r ? \n / ) ;
47+
48+ let inFilesSection = false ;
49+ let inPackagesSection = false ;
50+ let currentFileKey = null ;
51+ let currentPackageFileKey = null ;
52+ let rootFileKey = null ;
53+
54+ const replaceWithRename = ( value ) => {
55+ const base = toBasename ( value ) ;
56+ if ( ! base ) {
57+ return value ;
58+ }
59+ const renamed = renameMap . get ( base ) ;
60+ if ( ! renamed || renamed === base ) {
61+ return value ;
62+ }
63+ return value . endsWith ( base )
64+ ? value . slice ( 0 , value . length - base . length ) + renamed
65+ : value . replace ( base , renamed ) ;
66+ } ;
67+
68+ const applyHashIfAvailable = ( existingLine , key , indentPattern ) => {
69+ if ( ! key ) {
70+ return existingLine ;
71+ }
72+ const hash = hashMap . get ( key ) ;
73+ if ( ! hash ) {
74+ return existingLine ;
75+ }
76+ return existingLine . replace ( indentPattern , `$1${ hash } ` ) ;
77+ } ;
78+
79+ for ( let index = 0 ; index < lines . length ; index += 1 ) {
80+ const line = lines [ index ] ;
81+ const trimmed = line . trim ( ) ;
82+
83+ if ( trimmed === 'files:' ) {
84+ inFilesSection = true ;
85+ inPackagesSection = false ;
86+ currentFileKey = null ;
87+ continue ;
88+ }
89+ if ( trimmed === 'packages:' ) {
90+ inFilesSection = false ;
91+ inPackagesSection = true ;
92+ currentPackageFileKey = null ;
93+ continue ;
94+ }
95+ if ( ! line . startsWith ( ' ' ) ) {
96+ inFilesSection = trimmed === '' ? inFilesSection : false ;
97+ if ( trimmed !== 'packages:' ) {
98+ inPackagesSection = false ;
99+ }
100+ }
101+
102+ if ( inFilesSection ) {
103+ const urlMatch = line . match ( / ^ \s * - \s + u r l : \s + ( .* ) $ / ) ;
104+ if ( urlMatch ) {
105+ const rawValue = urlMatch [ 1 ] ;
106+ const updatedValue = replaceWithRename ( rawValue ) ;
107+ if ( updatedValue !== rawValue ) {
108+ lines [ index ] = line . replace ( rawValue , updatedValue ) ;
109+ }
110+ currentFileKey = toBasename ( updatedValue ) ;
111+ continue ;
112+ }
113+
114+ const pathMatch = line . match ( / ^ \s + p a t h : \s + ( .* ) $ / ) ;
115+ if ( pathMatch ) {
116+ const rawValue = pathMatch [ 1 ] ;
117+ const updatedValue = replaceWithRename ( rawValue ) ;
118+ if ( updatedValue !== rawValue ) {
119+ lines [ index ] = line . replace ( rawValue , updatedValue ) ;
120+ }
121+ currentFileKey = toBasename ( updatedValue ) ;
122+ continue ;
123+ }
124+
125+ const shaMatch = line . match ( / ^ ( \s + s h a 5 1 2 : \s + ) ( .* ) $ / ) ;
126+ if ( shaMatch ) {
127+ lines [ index ] = applyHashIfAvailable ( line , currentFileKey , / ^ ( \s + s h a 5 1 2 : \s + ) .* / ) ;
128+ continue ;
129+ }
130+ continue ;
131+ }
132+
133+ if ( inPackagesSection ) {
134+ const packagePathMatch = line . match ( / ^ \s { 4 } p a t h : \s + ( .* ) $ / ) ;
135+ if ( packagePathMatch ) {
136+ const rawValue = packagePathMatch [ 1 ] ;
137+ const updatedValue = replaceWithRename ( rawValue ) ;
138+ if ( updatedValue !== rawValue ) {
139+ lines [ index ] = line . replace ( rawValue , updatedValue ) ;
140+ }
141+ currentPackageFileKey = toBasename ( updatedValue ) ;
142+ continue ;
143+ }
144+
145+ const packageShaMatch = line . match ( / ^ ( \s { 4 } s h a 5 1 2 : \s + ) .* / ) ;
146+ if ( packageShaMatch ) {
147+ lines [ index ] = applyHashIfAvailable ( line , currentPackageFileKey , / ^ ( \s { 4 } s h a 5 1 2 : \s + ) .* / ) ;
148+ continue ;
149+ }
150+ continue ;
151+ }
152+
153+ const rootPathMatch = line . match ( / ^ p a t h : \s + ( .* ) $ / ) ;
154+ if ( rootPathMatch ) {
155+ const rawValue = rootPathMatch [ 1 ] ;
156+ const updatedValue = replaceWithRename ( rawValue ) ;
157+ if ( updatedValue !== rawValue ) {
158+ lines [ index ] = line . replace ( rawValue , updatedValue ) ;
159+ }
160+ rootFileKey = toBasename ( updatedValue ) ;
161+ continue ;
162+ }
163+
164+ const rootShaMatch = line . match ( / ^ ( s h a 5 1 2 : \s + ) .* / ) ;
165+ if ( rootShaMatch ) {
166+ lines [ index ] = applyHashIfAvailable ( line , rootFileKey , / ^ ( s h a 5 1 2 : \s + ) .* / ) ;
167+ continue ;
168+ }
169+ }
170+
171+ const updatedContent = lines . join ( '\n' ) ;
172+ if ( updatedContent !== raw ) {
173+ await fsPromises . writeFile ( manifestPath , updatedContent , 'utf8' ) ;
174+ log ( 'Updated manifest metadata.' , { manifest : path . basename ( manifestPath ) } ) ;
175+ } else {
176+ log ( 'Manifest already up to date.' , { manifest : path . basename ( manifestPath ) } ) ;
177+ }
178+ } ;
179+
25180const updateName = ( name ) => name . replace ( / - i a 3 2 - / gi, '-win32-' ) ;
26181
27182const main = async ( ) => {
@@ -31,14 +186,15 @@ const main = async () => {
31186 return ;
32187 }
33188
34- const entries = await fs . readdir ( releaseDir ) ;
189+ const entries = await fsPromises . readdir ( releaseDir ) ;
35190 const ia32Executables = entries . filter ( name => / - i a 3 2 - / i. test ( name ) && name . endsWith ( '.exe' ) ) ;
36191 if ( ia32Executables . length === 0 ) {
37- log ( 'No ia32 Windows executables detected; nothing to normalize.' ) ;
38- return ;
192+ log ( 'No ia32 Windows executables detected; skipping rename but refreshing manifest checksums.' ) ;
193+ } else {
194+ log ( 'Detected ia32 executables that require normalization.' , ia32Executables ) ;
39195 }
40196
41- log ( 'Detected ia32 executables that require normalization.' , ia32Executables ) ;
197+ const renameMap = new Map ( ) ;
42198
43199 for ( const fileName of ia32Executables ) {
44200 const sourcePath = path . join ( releaseDir , fileName ) ;
@@ -47,8 +203,9 @@ const main = async () => {
47203 continue ;
48204 }
49205 const targetPath = path . join ( releaseDir , targetName ) ;
50- await fs . rename ( sourcePath , targetPath ) ;
206+ await fsPromises . rename ( sourcePath , targetPath ) ;
51207 log ( 'Renamed executable.' , { from : fileName , to : targetName } ) ;
208+ renameMap . set ( fileName , targetName ) ;
52209 }
53210
54211 const blockmaps = entries . filter ( name => / - i a 3 2 - / i. test ( name ) && name . endsWith ( '.exe.blockmap' ) ) ;
@@ -59,30 +216,46 @@ const main = async () => {
59216 continue ;
60217 }
61218 const targetPath = path . join ( releaseDir , targetName ) ;
62- await fs . rename ( sourcePath , targetPath ) ;
219+ await fsPromises . rename ( sourcePath , targetPath ) ;
63220 log ( 'Renamed blockmap.' , { from : blockmap , to : targetName } ) ;
221+ renameMap . set ( blockmap , targetName ) ;
64222 }
65223
66224 const manifestPath = path . join ( releaseDir , 'latest.yml' ) ;
225+ const fallbackManifest = path . join ( releaseDir , 'latest-win32.yml' ) ;
67226 const hasManifest = await exists ( manifestPath ) ;
68- if ( ! hasManifest ) {
69- const fallbackManifest = path . join ( releaseDir , 'latest-win32.yml' ) ;
70- if ( await exists ( fallbackManifest ) ) {
71- log ( 'Found existing latest-win32.yml manifest; normalization already applied.' ) ;
72- return ;
227+ const hasFallback = await exists ( fallbackManifest ) ;
228+
229+ if ( ! hasFallback && hasManifest ) {
230+ await fsPromises . copyFile ( manifestPath , fallbackManifest ) ;
231+ log ( 'Seeded latest-win32.yml from latest.yml for normalization.' ) ;
232+ }
233+
234+ const hashes = new Map ( ) ;
235+ const updatedEntries = await fsPromises . readdir ( releaseDir ) ;
236+ for ( const entry of updatedEntries ) {
237+ if ( / \. e x e ( \. b l o c k m a p ) ? $ / i. test ( entry ) ) {
238+ const fullPath = path . join ( releaseDir , entry ) ;
239+ hashes . set ( entry , await computeSha512 ( fullPath ) ) ;
73240 }
74- log ( 'No latest.yml manifest found for ia32 build; cannot update metadata.' ) ;
75- return ;
76241 }
77242
78- const manifestRaw = await fs . readFile ( manifestPath , 'utf8' ) ;
79- const normalizedManifest = manifestRaw . replace ( / - i a 3 2 - / gi, '-win32-' ) ;
80- const targetManifestPath = path . join ( releaseDir , 'latest-win32.yml' ) ;
81- await fs . writeFile ( targetManifestPath , normalizedManifest , 'utf8' ) ;
82- log ( 'Wrote normalized manifest.' , { path : path . relative ( process . cwd ( ) , targetManifestPath ) } ) ;
243+ const manifestsToUpdate = [ ] ;
244+ if ( await exists ( manifestPath ) ) {
245+ manifestsToUpdate . push ( manifestPath ) ;
246+ }
247+ if ( await exists ( fallbackManifest ) ) {
248+ manifestsToUpdate . push ( fallbackManifest ) ;
249+ }
250+
251+ if ( manifestsToUpdate . length === 0 ) {
252+ log ( 'No manifest files found to update; please verify the release output.' ) ;
253+ return ;
254+ }
83255
84- await fs . unlink ( manifestPath ) ;
85- log ( 'Removed original latest.yml manifest to prevent mismatched metadata.' ) ;
256+ for ( const manifest of manifestsToUpdate ) {
257+ await updateManifestFile ( manifest , renameMap , hashes ) ;
258+ }
86259} ;
87260
88261main ( ) . catch ( error => {
0 commit comments