11/* eslint-disable no-console */
22import { Command } from 'commander'
33import { execSync } from 'child_process'
4- import { existsSync , readdirSync } from 'fs'
5- import { basename , extname , join , resolve } from 'path'
4+ import { existsSync , mkdirSync , readdirSync , readFileSync , statSync } from 'fs'
5+ import { basename , dirname , extname , join , relative , resolve } from 'path'
66import { checkLeapInstallation } from './chain/install'
77
88/**
@@ -25,19 +25,55 @@ export async function compileContract(file: string | undefined, outputDir: strin
2525 // Ensure cdt-cpp is installed
2626 await ensureCdtCppInstalled ( )
2727
28- console . log ( `Compiling ${ files . length } file(s) to ${ absoluteOutputDir } \n` )
28+ console . log ( `Compiling ${ files . length } file(s)... \n` )
2929
3030 for ( const filePath of files ) {
31- await compileSingleFile ( filePath , absoluteOutputDir )
31+ await compileSingleFile ( filePath , currentDir , absoluteOutputDir )
3232 }
3333
3434 console . log ( '\nCompilation complete!' )
3535}
3636
37+ /**
38+ * Recursively find all .cpp files in a directory
39+ */
40+ function findCppFilesRecursive ( dir : string , files : string [ ] = [ ] ) : string [ ] {
41+ try {
42+ const entries = readdirSync ( dir , { withFileTypes : true } )
43+
44+ for ( const entry of entries ) {
45+ const fullPath = join ( dir , entry . name )
46+
47+ // Skip hidden directories and common build/output directories
48+ if ( entry . isDirectory ( ) ) {
49+ if (
50+ entry . name . startsWith ( '.' ) ||
51+ entry . name === 'node_modules' ||
52+ entry . name === 'build' ||
53+ entry . name === 'dist' ||
54+ entry . name === 'lib'
55+ ) {
56+ continue
57+ }
58+ findCppFilesRecursive ( fullPath , files )
59+ } else if ( entry . isFile ( ) && extname ( entry . name ) === '.cpp' ) {
60+ files . push ( fullPath )
61+ }
62+ }
63+ } catch {
64+ // Ignore permission errors and other filesystem errors
65+ }
66+
67+ return files
68+ }
69+
3770/**
3871 * Get list of files to compile
3972 */
40- async function getFilesToCompile ( file : string | undefined , currentDir : string ) : Promise < string [ ] > {
73+ export async function getFilesToCompile (
74+ file : string | undefined ,
75+ currentDir : string
76+ ) : Promise < string [ ] > {
4177 if ( file ) {
4278 const filePath = resolve ( currentDir , file )
4379 if ( ! existsSync ( filePath ) ) {
@@ -49,10 +85,8 @@ async function getFilesToCompile(file: string | undefined, currentDir: string):
4985 return [ filePath ]
5086 }
5187
52- // Get all .cpp files in current directory
53- const files = readdirSync ( currentDir )
54- . filter ( ( f ) => extname ( f ) === '.cpp' )
55- . map ( ( f ) => join ( currentDir , f ) )
88+ // Recursively find all .cpp files in current directory and subdirectories
89+ const files = findCppFilesRecursive ( currentDir )
5690
5791 return files
5892}
@@ -104,18 +138,217 @@ function isCdtCppInstalled(): boolean {
104138 }
105139}
106140
141+ /**
142+ * Find the contract root directory by walking up from the source file
143+ * Looks for common contract directory structures
144+ */
145+ export function findContractRoot ( sourceFile : string ) : string {
146+ let currentDir = dirname ( resolve ( sourceFile ) )
147+ const root = resolve ( '/' )
148+
149+ // Walk up the directory tree looking for contract structure indicators
150+ while ( currentDir !== root ) {
151+ // Check for common contract directory patterns
152+ const hasInclude = existsSync ( join ( currentDir , 'include' ) )
153+ const hasSrc = existsSync ( join ( currentDir , 'src' ) )
154+ const hasInc = existsSync ( join ( currentDir , 'inc' ) )
155+ const hasHeaders = existsSync ( join ( currentDir , 'headers' ) )
156+
157+ // If we find include directories or src directory, this might be the contract root
158+ if ( hasInclude || hasSrc || hasInc || hasHeaders ) {
159+ return currentDir
160+ }
161+
162+ // Also check if current directory contains .cpp files (might be contract root)
163+ try {
164+ const files = readdirSync ( currentDir )
165+ const hasCppFiles = files . some ( ( f ) => extname ( f ) === '.cpp' )
166+ if ( hasCppFiles && ( hasInclude || hasInc || hasHeaders ) ) {
167+ return currentDir
168+ }
169+ } catch {
170+ // Ignore permission errors
171+ }
172+
173+ currentDir = dirname ( currentDir )
174+ }
175+
176+ // If no contract root found, return the source file's directory
177+ return dirname ( resolve ( sourceFile ) )
178+ }
179+
180+ /**
181+ * Auto-detect include directories
182+ */
183+ export function detectIncludeDirectories ( sourceFile : string , contractRoot : string ) : string [ ] {
184+ const sourceDir = dirname ( resolve ( sourceFile ) )
185+ const includeDirs : string [ ] = [ ]
186+
187+ // Common include directory patterns
188+ const patterns = [
189+ join ( contractRoot , 'include' ) ,
190+ join ( contractRoot , 'inc' ) ,
191+ join ( contractRoot , 'headers' ) ,
192+ join ( sourceDir , 'include' ) ,
193+ join ( sourceDir , 'inc' ) ,
194+ join ( sourceDir , 'headers' ) ,
195+ ]
196+
197+ for ( const dir of patterns ) {
198+ if ( existsSync ( dir ) && statSync ( dir ) . isDirectory ( ) ) {
199+ const normalized = resolve ( dir )
200+ if ( ! includeDirs . includes ( normalized ) ) {
201+ includeDirs . push ( normalized )
202+ }
203+ }
204+ }
205+
206+ return includeDirs
207+ }
208+
209+ /**
210+ * Auto-detect resource paths (for quoted includes like #include "actions/commit.cpp")
211+ */
212+ export function detectResourcePaths ( sourceFile : string , contractRoot : string ) : string [ ] {
213+ const sourceDir = dirname ( resolve ( sourceFile ) )
214+ const resourcePaths : string [ ] = [ ]
215+
216+ // Common resource directory patterns
217+ const patterns = [
218+ join ( contractRoot , 'src' ) ,
219+ join ( sourceDir , 'src' ) ,
220+ contractRoot , // Root itself might contain resources
221+ sourceDir , // Source directory might contain resources
222+ ]
223+
224+ for ( const dir of patterns ) {
225+ if ( existsSync ( dir ) && statSync ( dir ) . isDirectory ( ) ) {
226+ const normalized = resolve ( dir )
227+ if ( ! resourcePaths . includes ( normalized ) ) {
228+ resourcePaths . push ( normalized )
229+ }
230+ }
231+ }
232+
233+ return resourcePaths
234+ }
235+
236+ /**
237+ * Parse #include statements from source file
238+ */
239+ export function parseIncludes ( sourceFile : string ) : { angleBracket : string [ ] ; quoted : string [ ] } {
240+ const content = readFileSync ( sourceFile , 'utf8' )
241+ const angleBracket : string [ ] = [ ]
242+ const quoted : string [ ] = [ ]
243+
244+ // Match #include <...> and #include "..."
245+ const includeRegex = / # i n c l u d e \s + [ < " ] ( [ ^ > " ] + ) [ > " ] / g
246+ let match
247+
248+ while ( ( match = includeRegex . exec ( content ) ) !== null ) {
249+ const includePath = match [ 1 ]
250+ if ( match [ 0 ] . includes ( '<' ) ) {
251+ angleBracket . push ( includePath )
252+ } else {
253+ quoted . push ( includePath )
254+ }
255+ }
256+
257+ return { angleBracket, quoted}
258+ }
259+
260+ /**
261+ * Auto-detect compilation flags based on source file and directory structure
262+ */
263+ export function autoDetectCompileFlags ( sourceFile : string ) : string [ ] {
264+ const contractRoot = findContractRoot ( sourceFile )
265+ const includeDirs = detectIncludeDirectories ( sourceFile , contractRoot )
266+ const resourcePaths = detectResourcePaths ( sourceFile , contractRoot )
267+ const includes = parseIncludes ( sourceFile )
268+
269+ const flags : string [ ] = [ ]
270+
271+ // Add include directories (-I flag) if they exist
272+ // These are needed for angle-bracket includes like <randomrng/randomrng.hpp>
273+ if ( includeDirs . length > 0 ) {
274+ for ( const dir of includeDirs ) {
275+ flags . push ( `-I${ dir } ` )
276+ }
277+ }
278+
279+ // Add resource paths (-R flag) for quoted includes like "actions/commit.cpp"
280+ // Only add if there are actually quoted includes in the source
281+ if ( resourcePaths . length > 0 && includes . quoted . length > 0 ) {
282+ for ( const path of resourcePaths ) {
283+ flags . push ( `-R${ path } ` )
284+ }
285+ }
286+
287+ return flags
288+ }
289+
107290/**
108291 * Compile a single C++ file to WASM using cdt-cpp
109292 */
110- async function compileSingleFile ( filePath : string , outputDir : string ) : Promise < void > {
293+ async function compileSingleFile (
294+ filePath : string ,
295+ currentDir : string ,
296+ outputDir : string
297+ ) : Promise < void > {
111298 const fileName = basename ( filePath , '.cpp' )
112- const wasmOutput = join ( outputDir , `${ fileName } .wasm` )
299+ const contractRoot = findContractRoot ( filePath )
300+ const contractRootSrc = join ( contractRoot , 'src' )
301+ const sourceFileAbsolute = resolve ( filePath )
302+
303+ // Check if source file is inside a src/ directory relative to contract root
304+ // If so, strip the src/ prefix from output path
305+ let relativePath : string
306+ if ( existsSync ( contractRootSrc ) && sourceFileAbsolute . startsWith ( resolve ( contractRootSrc ) ) ) {
307+ // File is in src/ directory, calculate path relative to contract root
308+ const pathFromContractRoot = relative ( contractRoot , filePath )
309+ // Strip 'src/' prefix if present
310+ if ( pathFromContractRoot . startsWith ( 'src/' ) ) {
311+ relativePath = pathFromContractRoot . substring ( 4 ) // Remove 'src/' prefix
312+ } else {
313+ relativePath = pathFromContractRoot
314+ }
315+ } else {
316+ // Use path relative to current directory (preserve structure)
317+ relativePath = relative ( currentDir , filePath )
318+ }
319+
320+ const relativeDir = dirname ( relativePath )
321+
322+ // Calculate output path
323+ let wasmOutput : string
324+ if ( relativeDir === '.' || relativeDir === '' ) {
325+ // File should be output directly in output directory
326+ wasmOutput = join ( outputDir , `${ fileName } .wasm` )
327+ } else {
328+ // File is in a subdirectory, preserve the structure (but without src/ prefix if stripped)
329+ const outputSubDir = join ( outputDir , relativeDir )
330+ // Ensure the output subdirectory exists
331+ if ( ! existsSync ( outputSubDir ) ) {
332+ mkdirSync ( outputSubDir , { recursive : true } )
333+ }
334+ wasmOutput = join ( outputSubDir , `${ fileName } .wasm` )
335+ }
336+
337+ // Calculate ABI output path (same location as WASM, but with .abi extension)
338+ const abiOutput = wasmOutput . replace ( / \. w a s m $ / , '.abi' )
113339
114340 console . log ( `Compiling: ${ filePath } ` )
115341 console . log ( `Output: ${ wasmOutput } ` )
116342
343+ // Auto-detect compilation flags
344+ const autoFlags = autoDetectCompileFlags ( filePath )
345+ if ( autoFlags . length > 0 ) {
346+ console . log ( `Auto-detected flags: ${ autoFlags . join ( ' ' ) } ` )
347+ }
348+
117349 try {
118- const command = `cdt-cpp -abigen -o "${ wasmOutput } " "${ filePath } "`
350+ const flagsStr = autoFlags . length > 0 ? `${ autoFlags . join ( ' ' ) } ` : ''
351+ const command = `cdt-cpp -abigen -abigen_output="${ abiOutput } " ${ flagsStr } -o "${ wasmOutput } " "${ filePath } "`
119352 execSync ( command , {
120353 stdio : 'inherit' ,
121354 cwd : process . cwd ( ) ,
0 commit comments