1616import os
1717import subprocess
1818from pathlib import Path
19+ from typing import Dict
1920import argparse
2021import shutil
2122import logging
4243REPO_ROOT = SCRIPT_DIR .parent
4344LIND_WASM_BASE = Path (os .environ .get ("LIND_WASM_BASE" , REPO_ROOT )).resolve ()
4445LIND_FS_ROOT = Path (os .environ .get ("LIND_FS_ROOT" , LIND_WASM_BASE / "src/RawPOSIX/tmp" )).resolve ()
46+ CC = os .environ .get ("CC" , "gcc" ) # C compiler, defaults to gcc
4547
4648LIND_TOOL_PATH = LIND_WASM_BASE / "scripts"
4749TEST_FILE_BASE = LIND_WASM_BASE / "tests" / "unit-tests"
6365 "Lind_wasm_Segmentation_Fault" : "Lind Wasm Segmentation Failure" ,
6466 "Lind_wasm_Timeout" : "Timeout During Lind Wasm run" ,
6567 "Unknown_Failure" : "Unknown Failure" ,
66- "Output_mismatch" : "GCC and Wasm Output mismatch"
68+ "Output_mismatch" : "C Compiler and Wasm Output mismatch"
6769 }
6870
6971# ----------------------------------------------------------------------
@@ -223,16 +225,42 @@ def compile_and_run_native(source_file, timeout_sec=DEFAULT_TIMEOUT):
223225 source_file = Path (source_file )
224226 native_output = source_file .parent / f"{ source_file .stem } .o"
225227
228+ # Prepare any executable dependencies required by this test (like execv targets)
229+ executable_backups : Dict [Path , Path ] = {}
230+ created_native_execs = set ()
231+ native_dependencies = analyze_executable_dependencies ([source_file ])
232+ for exec_path , dependency_source in native_dependencies .items ():
233+ dest_path = (LIND_FS_ROOT / exec_path ).resolve ()
234+ dest_path .parent .mkdir (parents = True , exist_ok = True )
235+
236+ if dest_path .exists ():
237+ fd , backup_path = tempfile .mkstemp (prefix = f"{ dest_path .name } _orig_" , dir = str (dest_path .parent ))
238+ os .close (fd )
239+ shutil .copy2 (str (dest_path ), backup_path )
240+ executable_backups [dest_path ] = Path (backup_path )
241+
242+ dep_compile_cmd = [CC , str (dependency_source ), "-o" , str (dest_path )]
243+ try :
244+ dep_proc = run_subprocess (dep_compile_cmd , label = "native dep compile" , shell = False )
245+ except Exception as e :
246+ return False , f"Exception compiling dependency { dependency_source } : { e } " , "compile_error" , "Failure_native_compiling"
247+
248+ if dep_proc .returncode != 0 :
249+ error_output = dep_proc .stdout + dep_proc .stderr
250+ return False , f"Failed to compile dependency { dependency_source } : { error_output } " , "compile_error" , "Failure_native_compiling"
251+
252+ created_native_execs .add (dest_path )
253+
226254 # Ensure paths are absolute to prevent cwd confusion
227255 if not source_file .is_absolute ():
228256 raise ValueError (f"Source file must be absolute path, got: { source_file } " )
229257 if not native_output .is_absolute ():
230258 raise ValueError (f"Native output path must be absolute, got: { native_output } " )
231259
232260 # Compile
233- compile_cmd = ["gcc" , str (source_file ), "-o" , str (native_output )]
261+ compile_cmd = [CC , str (source_file ), "-o" , str (native_output )]
234262 try :
235- proc = run_subprocess (compile_cmd , label = "gcc compile" , cwd = LIND_FS_ROOT , shell = False )
263+ proc = run_subprocess (compile_cmd , label = f" { CC } compile" , cwd = LIND_FS_ROOT , shell = False )
236264 if proc .returncode != 0 :
237265 return False , proc .stdout + proc .stderr , "compile_error" , "Failure_native_compiling"
238266 except Exception as e :
@@ -251,8 +279,21 @@ def compile_and_run_native(source_file, timeout_sec=DEFAULT_TIMEOUT):
251279 return False , f"Exception: { e } " , "unknown_error" , "Failure_native_running"
252280 finally :
253281 # Clean up native binary
254- if native_output .exists ():
255- native_output .unlink ()
282+ native_output .unlink (missing_ok = True )
283+
284+ # Restore any executable dependencies that were swapped out
285+ for dest_path in created_native_execs :
286+ try :
287+ dest_path .unlink (missing_ok = True )
288+ except (FileNotFoundError , PermissionError ) as cleanup_err :
289+ logger .debug (f"Failed to remove native dependency { dest_path } : { cleanup_err } " )
290+
291+ for dest_path , backup_path in executable_backups .items ():
292+ try :
293+ if backup_path .exists ():
294+ shutil .move (str (backup_path ), str (dest_path ))
295+ except (FileNotFoundError , PermissionError ) as restore_err :
296+ logger .warning (f"Failed to restore dependency { dest_path } from backup: { restore_err } " )
256297
257298# ----------------------------------------------------------------------
258299# Function: get_expected_output
@@ -474,8 +515,8 @@ def analyze_testfile_dependencies(tests_to_run):
474515 with open (test_file , 'r' ) as f :
475516 content = f .read ()
476517
477- # Look for testfiles references in open() calls
478- testfile_pattern = r'open\s*\(\s* "testfiles/([^"]+)"'
518+ # Look for any " testfiles/..." string literals in the code
519+ testfile_pattern = r'"testfiles/([^"]+)"'
479520 matches = re .findall (testfile_pattern , content , re .IGNORECASE )
480521 all_dependencies .update (matches )
481522
@@ -487,6 +528,102 @@ def analyze_testfile_dependencies(tests_to_run):
487528
488529 return all_dependencies
489530
531+ # ----------------------------------------------------------------------
532+ # Function: analyze_executable_dependencies
533+ #
534+ # Purpose:
535+ # Analyzes test files to determine which executables they need
536+ #
537+ # Variables:
538+ # - Input:
539+ # tests_to_run: List of test files to analyze
540+ # - Output:
541+ # Dictionary mapping executable paths to their source file paths
542+ # e.g., {'automated_tests/hello-arg': Path('hello-arg.c')}
543+ # ----------------------------------------------------------------------
544+ def analyze_executable_dependencies (tests_to_run ):
545+ import re
546+
547+ executable_deps : Dict [str , Path ] = {}
548+
549+ for test_file in tests_to_run :
550+ try :
551+ with open (test_file , 'r' ) as f :
552+ content = f .read ()
553+
554+ # Look for execv/execve/execl calls with string literal paths
555+ # NOTE: This intentionally only matches simple string literals like execv("path", ...)
556+ # It does NOT match variable references like execv(argv[0], ...) or macro expansions
557+ # This is a deliberate limitation to keep the dependency analysis simple and predictable
558+ # Pattern matches: execv("path/to/executable", ...), execl("/bin/ls", ...), etc.
559+ exec_pattern = r'exec[vle]+\s*\(\s*"([^"]+)"'
560+ matches = re .findall (exec_pattern , content , re .IGNORECASE )
561+
562+ for exec_path in matches :
563+ exec_name = Path (exec_path ).name
564+
565+ candidate_sources = [
566+ test_file .parent / f"{ exec_name } .c" ,
567+ test_file .resolve ().parent / f"{ exec_name } .c"
568+ ]
569+
570+ selected_source = None
571+ for candidate in candidate_sources :
572+ if candidate .exists ():
573+ selected_source = candidate
574+ break
575+
576+ if selected_source :
577+ executable_deps [exec_path ] = selected_source
578+ logger .debug (f"Found executable dependency in { test_file .name } : { exec_path } -> { selected_source .name } " )
579+ else :
580+ logger .debug (
581+ f"Executable { exec_path } referenced but no matching source found near { test_file } "
582+ )
583+
584+ except Exception as e :
585+ logger .debug (f"Could not analyze executable dependencies for { test_file } : { e } " )
586+
587+ return executable_deps
588+
589+ # ----------------------------------------------------------------------
590+ # Function: create_required_executables
591+ #
592+ # Purpose:
593+ # Compiles required executables and places them in LIND_FS_ROOT
594+ #
595+ # Variables:
596+ # - Input:
597+ # executable_deps: Dictionary mapping executable paths to source files
598+ # - Output:
599+ # None (creates executables in LIND_FS_ROOT)
600+ # ----------------------------------------------------------------------
601+ def create_required_executables (executable_deps ):
602+ if not executable_deps :
603+ return
604+
605+ logger .info (f"Creating { len (executable_deps )} required executable(s)" )
606+
607+ for exec_path , source_file in executable_deps .items ():
608+ try :
609+ # Compile the source file to WASM
610+ wasm_file , compile_err = compile_c_to_wasm (source_file )
611+
612+ if wasm_file is None :
613+ logger .error (f"Failed to compile { source_file } : { compile_err } " )
614+ continue
615+
616+ # Create destination directory in LIND_FS_ROOT
617+ dest_path = LIND_FS_ROOT / exec_path
618+ dest_path .parent .mkdir (parents = True , exist_ok = True )
619+
620+ # Copy the compiled WASM to the destination (preserves source for potential reuse)
621+ shutil .copy2 (str (wasm_file ), str (dest_path ))
622+ logger .info (f"Created executable: { dest_path } " )
623+
624+ except Exception as e :
625+ logger .error (f"Failed to create executable { exec_path } : { e } " )
626+
490627# ----------------------------------------------------------------------
491628# Function: pre_test
492629#
@@ -502,6 +639,9 @@ def analyze_testfile_dependencies(tests_to_run):
502639# None
503640# ----------------------------------------------------------------------
504641def pre_test (tests_to_run = None ):
642+ # Ensure LIND_FS_ROOT exists (For CI Environment)
643+ os .makedirs (LIND_FS_ROOT , exist_ok = True )
644+
505645 # If tests_to_run is provided, use selective copying
506646 if tests_to_run :
507647 all_dependencies = analyze_testfile_dependencies (tests_to_run )
@@ -548,6 +688,11 @@ def pre_test(tests_to_run=None):
548688 except OSError :
549689 # Fallback to copying in case symlink creation fails
550690 shutil .copy2 (readlinkfile_path , symlink_path )
691+
692+ # Create required executables
693+ if tests_to_run :
694+ executable_deps = analyze_executable_dependencies (tests_to_run )
695+ create_required_executables (executable_deps )
551696
552697# ----------------------------------------------------------------------
553698# Function: generate_html_report
0 commit comments