1+ import fastify from 'fastify' ;
2+ import fs from 'fs' ;
3+ import { mkdirSync , writeFileSync , existsSync , openSync , closeSync , readFileSync , renameSync , rmSync , unlinkSync } from "fs" ;
4+ import { deflateSync } from "zlib" ;
5+ import { execSync } from "child_process" ;
6+ import { z } from 'zod' ;
7+ import fastifyCors from 'fastify-cors' ;
8+ import fastifyWebSocket from 'fastify-websocket' ;
9+
10+ const server = fastify ( ) ;
11+
12+ server . register ( fastifyCors , {
13+ // put your options here
14+ origin : '*'
15+ } )
16+ server . register ( fastifyWebSocket ) ;
17+
18+ // Compilation code
19+ const llvmDir = process . cwd ( ) + "/clang/wasi-sdk" ;
20+ const tempDir = "/tmp" ;
21+ const sysroot = llvmDir + "/share/wasi-sysroot" ;
22+
23+ export interface ResponseData {
24+ success : boolean ;
25+ message : string ;
26+ output : string ;
27+ tasks : Task [ ] ;
28+ }
29+
30+ export interface Task {
31+ name : string ;
32+ file ?: string ;
33+ success ?: boolean ;
34+ console ?: string ;
35+ output ?: string ;
36+ }
37+
38+ const requestBodySchema = z . object ( {
39+ output : z . enum ( [ 'wasm' ] ) ,
40+ files : z . array ( z . object ( {
41+ type : z . string ( ) ,
42+ name : z . string ( ) ,
43+ options : z . string ( ) . optional ( ) ,
44+ src : z . string ( )
45+ } ) ) ,
46+ link_options : z . string ( ) . optional ( ) ,
47+ compress : z . boolean ( ) . optional ( ) ,
48+ strip : z . boolean ( ) . optional ( )
49+ } ) ;
50+
51+ type RequestBody = z . infer < typeof requestBodySchema > ;
52+
53+ // Input: JSON in the following format
54+ // {
55+ // output: "wasm",
56+ // files: [
57+ // {
58+ // type: "c",
59+ // name: "file.c",
60+ // options: "-O3 -std=c99",
61+ // src: "#include..."
62+ // }
63+ // ],
64+ // link_options: "--import-memory"
65+ // }
66+ // Output: JSON in the following format
67+ // {
68+ // success: true,
69+ // message: "Success",
70+ // output: "AGFzbQE.... =",
71+ // tasks: [
72+ // {
73+ // name: "building wasm",
74+ // success: true,
75+ // console: ""
76+ // }
77+ // ]
78+ // }
79+
80+ function sanitize_shell_output < T > ( out : T ) : T {
81+ return out ; // FIXME
82+ }
83+
84+ function shell_exec ( cmd : string , cwd : string ) {
85+ const out = openSync ( cwd + '/out.log' , 'w' ) ;
86+ let error = '' ;
87+ try {
88+ execSync ( cmd , { cwd, stdio : [ null , out , out ] , } ) ;
89+ } catch ( ex : unknown ) {
90+ if ( ex instanceof Error ) {
91+ error = ex ?. message ;
92+ }
93+ } finally {
94+ closeSync ( out ) ;
95+ }
96+ const result = readFileSync ( cwd + '/out.log' ) . toString ( ) || error ;
97+ return result ;
98+ }
99+
100+ function get_optimization_options ( options : string ) {
101+ const optimization_options = [
102+ /* default '-O0' not included */ '-O1' , '-O2' , '-O3' , '-O4' , '-Os'
103+ ] ;
104+
105+ let safe_options = '' ;
106+ for ( let o of optimization_options ) {
107+ if ( options . includes ( o ) ) {
108+ safe_options += ' ' + o ;
109+ }
110+ }
111+
112+ return safe_options ;
113+ }
114+
115+ function get_clang_options ( options : string ) {
116+ const clang_flags = `--sysroot=${ sysroot } -xc -I/app/clang/includes -fdiagnostics-print-source-range-info -Werror=implicit-function-declaration` ;
117+ const miscellaneous_options = [
118+ '-ffast-math' , '-fno-inline' , '-std=c99' , '-std=c89'
119+ ] ;
120+
121+ let safe_options = '' ;
122+ for ( let o of miscellaneous_options ) {
123+ if ( options . includes ( o ) ) {
124+ safe_options += ' ' + o ;
125+ } else if ( o . includes ( '-std=' ) && options . toLowerCase ( ) . includes ( o ) ) {
126+ safe_options += ' ' + o ;
127+ }
128+ }
129+
130+ return clang_flags + safe_options ;
131+ }
132+
133+ function get_lld_options ( options : string ) {
134+ // --sysroot=${sysroot} is already included in compiler options
135+ const clang_flags = `--no-standard-libraries -nostartfiles -Wl,--allow-undefined,--no-entry,--export-all` ;
136+ if ( ! options ) {
137+ return clang_flags ;
138+ }
139+ const available_options = [ '--import-memory' , '-g' ] ;
140+ let safe_options = '' ;
141+ for ( let o of available_options ) {
142+ if ( options . includes ( o ) ) {
143+ safe_options += ' -Wl,' + o ;
144+ }
145+ }
146+ return clang_flags + safe_options ;
147+ }
148+
149+ function serialize_file_data ( filename : string , compress : boolean ) {
150+ let content = readFileSync ( filename ) ;
151+ if ( compress ) {
152+ content = deflateSync ( content ) ;
153+ }
154+ return content . toString ( "base64" ) ;
155+ }
156+
157+ function validate_filename ( name : string ) {
158+ if ( ! / ^ [ A - Z a - z 0 - 9 _ - ] + [ . ] [ A - Z a - z 0 - 9 ] { 1 , 4 } $ / . test ( name ) ) {
159+ return false ;
160+ }
161+ const parts = name . split ( / \/ / g) ;
162+ for ( let p of parts ) {
163+ if ( p == '.' || p == '..' ) {
164+ return false ;
165+ }
166+ }
167+ return parts ;
168+ }
169+
170+ function link_c_files ( source_files : string [ ] , compile_options : string , link_options : string , cwd : string , output : string , result_obj : Task ) {
171+ const files = source_files . join ( ' ' ) ;
172+ const clang = llvmDir + '/bin/clang' ;
173+ const cmd = clang + ' ' + get_clang_options ( compile_options ) + ' ' + get_lld_options ( link_options ) + ' ' + files + ' -o ' + output ;
174+ const out = shell_exec ( cmd , cwd ) ;
175+ result_obj . console = sanitize_shell_output ( out ) ;
176+ if ( ! existsSync ( output ) ) {
177+ result_obj . success = false ;
178+ return false ;
179+ }
180+ result_obj . success = true ;
181+ return true ;
182+ }
183+
184+ function optimize_wasm ( cwd : string , inplace : string , opt_options : string , result_obj : Task ) {
185+ const unopt = cwd + '/unopt.wasm' ;
186+ const cmd = 'wasm-opt ' + opt_options + ' -o ' + inplace + ' ' + unopt ;
187+ const out = openSync ( cwd + '/opt.log' , 'w' ) ;
188+ let error = '' ;
189+ let success = true ;
190+ try {
191+ renameSync ( inplace , unopt ) ;
192+ execSync ( cmd , { cwd, stdio : [ null , out , out ] , } ) ;
193+ } catch ( ex : unknown ) {
194+ success = false ;
195+ if ( ex instanceof Error ) {
196+ error = ex ?. message ;
197+ }
198+ } finally {
199+ closeSync ( out ) ;
200+ }
201+ const out_msg = readFileSync ( cwd + '/opt.log' ) . toString ( ) || error ;
202+ result_obj . console = sanitize_shell_output ( out_msg ) ;
203+ result_obj . success = success ;
204+ return success ;
205+ }
206+
207+ function clean_wasm ( cwd : string , inplace : string , result_obj : Task ) {
208+ const cmd = 'hook-cleaner ' + inplace ;
209+ const out = openSync ( cwd + '/cleanout.log' , 'w' ) ;
210+ let error = '' ;
211+ let success = true ;
212+ try {
213+ execSync ( cmd , { cwd, stdio : [ null , out , out ] , } ) ;
214+ } catch ( ex : unknown ) {
215+ success = false ;
216+ if ( ex instanceof Error ) {
217+ error = ex ?. message ;
218+ }
219+ } finally {
220+ closeSync ( out ) ;
221+ }
222+ const out_msg = readFileSync ( cwd + '/cleanout.log' ) . toString ( ) || error ;
223+ result_obj . console = sanitize_shell_output ( out_msg ) ;
224+ result_obj . success = success ;
225+ return success ;
226+ }
227+
228+ export function build_project ( project : RequestBody , base : string ) {
229+ const output = project . output ;
230+ const compress = project . compress ;
231+ const strip = project . strip ;
232+ let build_result : ResponseData = {
233+ success : false ,
234+ message : '' ,
235+ output : '' ,
236+ tasks : [ ] ,
237+ } ;
238+ const dir = base + '.$' ;
239+ const result = base + '.wasm' ;
240+
241+ const complete = ( success : boolean , message : string ) => {
242+ rmSync ( dir , { recursive : true } ) ;
243+ if ( existsSync ( result ) ) {
244+ unlinkSync ( result ) ;
245+ }
246+
247+ build_result . success = success ;
248+ build_result . message = message ;
249+ return build_result ;
250+ } ;
251+
252+ if ( output != 'wasm' ) {
253+ return complete ( false , 'Invalid output type ' + output ) ;
254+ }
255+
256+ build_result . tasks = [ ] ;
257+ const files = project . files ;
258+ if ( ! files . length ) {
259+ return complete ( false , 'No source files' ) ;
260+ }
261+
262+ if ( ! existsSync ( dir ) ) {
263+ mkdirSync ( dir ) ;
264+ }
265+
266+ const sources = [ ] ;
267+ let options ;
268+ for ( let file of files ) {
269+ const name = file . name ;
270+ if ( ! validate_filename ( name ) ) {
271+ return complete ( false , 'Invalid filename ' + name ) ;
272+ }
273+ const fileName = dir + '/' + name ;
274+ sources . push ( fileName ) ;
275+ if ( ! options ) {
276+ options = file . options ;
277+ } else {
278+ if ( file . options && ( file . options != options ) ) {
279+ return complete ( false , 'Per-file compilation options not supported' ) ;
280+ }
281+ }
282+
283+ const src = file . src ;
284+ if ( ! src ) {
285+ return complete ( false , 'Source file ' + name + ' is empty' ) ;
286+ }
287+
288+ writeFileSync ( fileName , src ) ;
289+ }
290+ const link_options = project . link_options ;
291+ const link_result_obj = {
292+ name : 'building wasm'
293+ } ;
294+ build_result . tasks . push ( link_result_obj ) ;
295+ if ( ! link_c_files ( sources , options || '' , link_options || '' , dir , result , link_result_obj ) ) {
296+ return complete ( false , 'Build error' ) ;
297+ }
298+
299+ const opt_options = get_optimization_options ( options || '' ) ;
300+ if ( opt_options ) {
301+ const opt_obj = {
302+ name : 'optimizing wasm'
303+ } ;
304+ build_result . tasks . push ( opt_obj ) ;
305+ if ( ! optimize_wasm ( dir , result , opt_options , opt_obj ) ) {
306+ return complete ( false , 'Optimization error' ) ;
307+ }
308+ }
309+
310+ if ( strip ) {
311+ const clean_obj = {
312+ name : 'cleaning wasm'
313+ } ;
314+ build_result . tasks . push ( clean_obj ) ;
315+ if ( ! clean_wasm ( dir , result , clean_obj ) ) {
316+ return complete ( false , 'Post-build error' ) ;
317+ }
318+ }
319+
320+ build_result . output = serialize_file_data ( result , compress || false ) ;
321+
322+ return complete ( true , 'Success' ) ;
323+ }
324+ // END Compile code
0 commit comments