@@ -3,23 +3,29 @@ import {readFileSync, writeFileSync} from 'node:fs';
33import { dirname , resolve } from 'node:path' ;
44import { fileURLToPath } from 'node:url' ;
55
6- const SOURCE_COMMIT = '8efaf11d2113e5d2d0ef0a8b0b710703d296c153' ;
7- const SOURCE_SHA256 = '60f37ce619fac19905b760efce96195c7f4c683b2bca7f85777a8825ab613e19' ;
8- const OUTPUT_SHA256 = '1d959e0e5efccd470c1a1ce79bcacc066cfa38499237955b0ec382d00245f8ec' ;
9-
10- const sourcePath = process . argv [ 2 ] ;
11- if ( ! sourcePath ) {
12- throw new Error ( 'Usage: node scripts/vendor-tlottie.mjs /path/to/tlottie/examples/web/tlottie.wasm' ) ;
6+ const SOURCE_COMMIT = 'cbfaf4fa180a74aec826ac2662e83d3ae0bbc560' ;
7+
8+ // Upstream builds the same sources twice: with and without `-C target-feature=simd128`.
9+ // Both variants are vendored so browsers without WebAssembly SIMD still get a renderer.
10+ const VARIANTS = [ {
11+ name : 'tlottie.wasm' ,
12+ sourceSha256 : '48e7ad6025cdae153214ea32ec4393c446475df7a3d5939d8cc286f7b9979248' ,
13+ outputSha256 : '0cb9c73e2e184d3c3d2762d4f7c23ed1c993a76a9f0c10f1ea84883a4dc41801' ,
14+ simd : true
15+ } , {
16+ name : 'tlottie.nosimd.wasm' ,
17+ sourceSha256 : '17bebc9128dcc3351a405a47192af7ee2c5d4653869fd76ef504c75681d242d7' ,
18+ outputSha256 : '01e0d8359073259cb6aed1a61e97ceee964419cdfecb2c8849a8deae6b6b934c' ,
19+ simd : false
20+ } ] ;
21+
22+ const sourceDir = process . argv [ 2 ] ;
23+ if ( ! sourceDir ) {
24+ throw new Error ( 'Usage: node scripts/vendor-tlottie.mjs /path/to/tlottie/examples/web' ) ;
1325}
1426
1527const sha256 = ( bytes ) => createHash ( 'sha256' ) . update ( bytes ) . digest ( 'hex' ) ;
1628
17- const source = readFileSync ( sourcePath ) ;
18- const sourceHash = sha256 ( source ) ;
19- if ( sourceHash !== SOURCE_SHA256 ) {
20- throw new Error ( `Expected tlottie ${ SOURCE_COMMIT } (${ SOURCE_SHA256 } ), got ${ sourceHash } ` ) ;
21- }
22-
2329const readVarUint32 = ( bytes , start ) => {
2430 let offset = start ;
2531 let value = 0 ;
@@ -38,50 +44,99 @@ const readVarUint32 = (bytes, start) => {
3844 throw new Error ( 'Invalid WebAssembly varuint32' ) ;
3945} ;
4046
41- // Drop only debug/name custom sections from the upstream web artifact;
42- // executable sections and target metadata remain byte-for-byte.
43- const chunks = [ source . subarray ( 0 , 8 ) ] ;
44- let offset = 8 ;
45- while ( offset < source . length ) {
46- const sectionStart = offset ;
47- const sectionId = source [ offset ++ ] ;
48- const length = readVarUint32 ( source , offset ) ;
49- const payloadStart = length . offset ;
50- const sectionEnd = payloadStart + length . value ;
51- if ( sectionEnd > source . length ) {
52- throw new Error ( 'Invalid WebAssembly section length' ) ;
53- }
47+ const readSections = ( bytes ) => {
48+ const sections = [ ] ;
49+ let offset = 8 ;
50+ while ( offset < bytes . length ) {
51+ const sectionStart = offset ;
52+ const sectionId = bytes [ offset ++ ] ;
53+ const length = readVarUint32 ( bytes , offset ) ;
54+ const payloadStart = length . offset ;
55+ const sectionEnd = payloadStart + length . value ;
56+ if ( sectionEnd > bytes . length ) {
57+ throw new Error ( 'Invalid WebAssembly section length' ) ;
58+ }
59+
60+ let name ;
61+ if ( sectionId === 0 ) {
62+ const nameLength = readVarUint32 ( bytes , payloadStart ) ;
63+ const nameEnd = nameLength . offset + nameLength . value ;
64+ if ( nameEnd > sectionEnd ) {
65+ throw new Error ( 'Invalid WebAssembly custom section name' ) ;
66+ }
5467
55- let keep = true ;
56- if ( sectionId === 0 ) {
57- const nameLength = readVarUint32 ( source , payloadStart ) ;
58- const nameEnd = nameLength . offset + nameLength . value ;
59- if ( nameEnd > sectionEnd ) {
60- throw new Error ( 'Invalid WebAssembly custom section name' ) ;
68+ name = bytes . subarray ( nameLength . offset , nameEnd ) . toString ( ) ;
6169 }
6270
63- const name = source . subarray ( nameLength . offset , nameEnd ) . toString ( ) ;
64- keep = ! name . startsWith ( '.debug_' ) && name !== 'name' ;
71+ sections . push ( { sectionId , name, payloadStart , sectionStart , sectionEnd } ) ;
72+ offset = sectionEnd ;
6573 }
6674
67- if ( keep ) {
68- chunks . push ( source . subarray ( sectionStart , sectionEnd ) ) ;
69- }
75+ return sections ;
76+ } ;
7077
71- offset = sectionEnd ;
72- }
78+ // rustc records the enabled target features; it is the only place the two variants
79+ // are told apart, so the vendored binary has to keep it and match the requested build.
80+ const readTargetFeatures = ( bytes , section ) => {
81+ const features = [ ] ;
82+ let offset = section . payloadStart ;
83+ const name = readVarUint32 ( bytes , offset ) ;
84+ offset = name . offset + name . value ;
85+
86+ const count = readVarUint32 ( bytes , offset ) ;
87+ offset = count . offset ;
88+ for ( let i = 0 ; i < count . value ; ++ i ) {
89+ const prefix = String . fromCharCode ( bytes [ offset ++ ] ) ;
90+ const length = readVarUint32 ( bytes , offset ) ;
91+ features . push ( prefix + bytes . subarray ( length . offset , length . offset + length . value ) . toString ( ) ) ;
92+ offset = length . offset + length . value ;
93+ }
7394
74- const output = Buffer . concat ( chunks ) ;
75- new WebAssembly . Module ( output ) ;
76- const outputHash = sha256 ( output ) ;
77- if ( outputHash !== OUTPUT_SHA256 ) {
78- throw new Error ( `Expected vendored tlottie ${ OUTPUT_SHA256 } , got ${ outputHash } ` ) ;
79- }
95+ return features ;
96+ } ;
8097
8198const rootDir = dirname ( dirname ( fileURLToPath ( import . meta. url ) ) ) ;
82- const outputPath = resolve ( rootDir , 'src/vendor/tlottie/tlottie.wasm' ) ;
83- writeFileSync ( outputPath , output ) ;
8499
85- console . log ( `Vendored tlottie ${ SOURCE_COMMIT } ` ) ;
86- console . log ( `source: ${ source . length } bytes, ${ sourceHash } ` ) ;
87- console . log ( `output: ${ output . length } bytes, ${ outputHash } ` ) ;
100+ for ( const variant of VARIANTS ) {
101+ const sourcePath = resolve ( sourceDir , variant . name ) ;
102+ const source = readFileSync ( sourcePath ) ;
103+ const sourceHash = sha256 ( source ) ;
104+ if ( sourceHash !== variant . sourceSha256 ) {
105+ throw new Error ( `Expected ${ variant . name } of tlottie ${ SOURCE_COMMIT } (${ variant . sourceSha256 } ), got ${ sourceHash } ` ) ;
106+ }
107+
108+ const sections = readSections ( source ) ;
109+ const targetFeatures = sections . find ( ( section ) => section . name === 'target_features' ) ;
110+ if ( ! targetFeatures ) {
111+ throw new Error ( `${ variant . name } has no target_features section` ) ;
112+ }
113+
114+ const hasSimd = readTargetFeatures ( source , targetFeatures ) . includes ( '+simd128' ) ;
115+ if ( hasSimd !== variant . simd ) {
116+ throw new Error ( `${ variant . name } ${ hasSimd ? 'requires' : 'does not require' } simd128, which contradicts the expected variant` ) ;
117+ }
118+
119+ // Drop only debug/name custom sections from the upstream web artifact;
120+ // executable sections and target metadata remain byte-for-byte.
121+ const chunks = [ source . subarray ( 0 , 8 ) ] ;
122+ for ( const section of sections ) {
123+ const keep = section . sectionId !== 0 ||
124+ ( ! section . name . startsWith ( '.debug_' ) && section . name !== 'name' ) ;
125+ if ( keep ) {
126+ chunks . push ( source . subarray ( section . sectionStart , section . sectionEnd ) ) ;
127+ }
128+ }
129+
130+ const output = Buffer . concat ( chunks ) ;
131+ new WebAssembly . Module ( output ) ;
132+ const outputHash = sha256 ( output ) ;
133+ if ( outputHash !== variant . outputSha256 ) {
134+ throw new Error ( `Expected vendored ${ variant . name } ${ variant . outputSha256 } , got ${ outputHash } ` ) ;
135+ }
136+
137+ writeFileSync ( resolve ( rootDir , 'src/vendor/tlottie' , variant . name ) , output ) ;
138+
139+ console . log ( `Vendored ${ variant . name } of tlottie ${ SOURCE_COMMIT } ` ) ;
140+ console . log ( `source: ${ source . length } bytes, ${ sourceHash } ` ) ;
141+ console . log ( `output: ${ output . length } bytes, ${ outputHash } ` ) ;
142+ }
0 commit comments