@@ -29,13 +29,17 @@ type BuildOptions struct {
2929}
3030
3131// DefaultBuildOptions returns default build options
32+ // Environment variables are checked with fallbacks, so the code works reliably
33+ // even if env vars are not set (os.Getenv returns empty string for unset vars).
3234func DefaultBuildOptions () * BuildOptions {
3335 linkFlags := []string {"-s" }
36+ // Check FERRET_AS first, then AS, then toolchain, then default to "as"
3437 assembler := os .Getenv ("FERRET_AS" )
3538 if assembler == "" {
3639 assembler = os .Getenv ("AS" )
3740 }
3841
42+ // Check FERRET_LD first, then LD, then toolchain, then default to "ld"
3943 linker := os .Getenv ("FERRET_LD" )
4044 if linker == "" {
4145 linker = os .Getenv ("LD" )
@@ -244,6 +248,10 @@ func ensureToolExists(kind, tool string) error {
244248 return fmt .Errorf ("%s not found: %s" , kind , tool )
245249}
246250
251+ // applyLdDefaults sets up platform-specific linker defaults.
252+ // This function only runs for GNU ld linkers (checked via isLdLinker).
253+ // For other linkers (LLD, Gold, clang's linker), users should configure
254+ // linking manually via FERRET_LD_FLAGS and FERRET_LD_LIBS environment variables.
247255func applyLdDefaults (opts * BuildOptions ) error {
248256 if opts == nil || opts .LdDefaults || ! isLdLinker (opts .Linker ) {
249257 return nil
@@ -277,6 +285,44 @@ func applyLdDefaults(opts *BuildOptions) error {
277285 if ! containsArg (opts .LinkLibs , "-lc" ) {
278286 opts .LinkLibs = append ([]string {"-lc" }, opts .LinkLibs ... )
279287 }
288+ // Link libgcc for compiler runtime functions (e.g., 128-bit float operations)
289+ // Try to find static libgcc.a first, then fall back to shared libgcc_s.so.1
290+ libgccLinked := false
291+ if tcLib := toolchainLibDir (); tcLib != "" {
292+ // Check for static libgcc.a in toolchain (though bootstrap doesn't copy it)
293+ if path := filepath .Join (tcLib , "libgcc.a" ); utilsfs .IsValidFile (path ) {
294+ opts .LinkLibs = append (opts .LinkLibs , "-l:libgcc.a" )
295+ libgccLinked = true
296+ }
297+ }
298+ if ! libgccLinked {
299+ // Try to find libgcc.a via gcc
300+ if cc := resolveCCompiler (); cc != "" {
301+ if path := gccPrintFile (cc , "libgcc.a" ); path != "" {
302+ // Add the directory to library search path and link
303+ libDir := filepath .Dir (path )
304+ if ! containsLibDir (opts .LinkFlags , libDir ) {
305+ opts .LinkFlags = append (opts .LinkFlags , "-L" , libDir )
306+ }
307+ opts .LinkLibs = append (opts .LinkLibs , "-lgcc" )
308+ libgccLinked = true
309+ }
310+ }
311+ }
312+ if ! libgccLinked {
313+ // Fall back to shared library (bootstrap copies libgcc_s.so.1)
314+ // Use -l: syntax (GNU ld specific) to link against exact filename
315+ // This works without needing symlinks and is safe because applyLdDefaults
316+ // only runs for GNU ld linkers
317+ if tcLib := toolchainLibDir (); tcLib != "" {
318+ if path := filepath .Join (tcLib , "libgcc_s.so.1" ); utilsfs .IsValidFile (path ) {
319+ opts .LinkLibs = append (opts .LinkLibs , "-l:libgcc_s.so.1" )
320+ libgccLinked = true
321+ }
322+ }
323+ }
324+ // Note: We don't need a fallback to -lgcc_s since -l:libgcc_s.so.1 works
325+ // without symlinks. If libgcc_s.so.1 doesn't exist, that's a bootstrap issue.
280326 opts .LinkPost = append (opts .LinkPost , crtn )
281327 case "android" :
282328 return fmt .Errorf ("android builds are not supported here; use install-termux.sh" )
@@ -297,6 +343,46 @@ func findLinuxCrtObjects() (string, string, string) {
297343 crti := os .Getenv ("FERRET_LD_CRTI" )
298344 crtn := os .Getenv ("FERRET_LD_CRTN" )
299345
346+ // First, check toolchain/lib directory (bundled files from bootstrap)
347+ if tcLib := toolchainLibDir (); tcLib != "" {
348+ if crt1 == "" {
349+ if path := filepath .Join (tcLib , "crt1.o" ); utilsfs .IsValidFile (path ) {
350+ crt1 = path
351+ }
352+ }
353+ if crti == "" {
354+ if path := filepath .Join (tcLib , "crti.o" ); utilsfs .IsValidFile (path ) {
355+ crti = path
356+ }
357+ }
358+ if crtn == "" {
359+ if path := filepath .Join (tcLib , "crtn.o" ); utilsfs .IsValidFile (path ) {
360+ crtn = path
361+ }
362+ }
363+ }
364+
365+ // If still missing, use gcc to dynamically find files
366+ cc := resolveCCompiler ()
367+ if cc != "" {
368+ if crt1 == "" {
369+ if path := gccPrintFile (cc , "crt1.o" ); path != "" {
370+ crt1 = path
371+ }
372+ }
373+ if crti == "" {
374+ if path := gccPrintFile (cc , "crti.o" ); path != "" {
375+ crti = path
376+ }
377+ }
378+ if crtn == "" {
379+ if path := gccPrintFile (cc , "crtn.o" ); path != "" {
380+ crtn = path
381+ }
382+ }
383+ }
384+
385+ // Last resort: check hardcoded paths
300386 dirs := linuxLibDirs ()
301387 if crt1 == "" {
302388 crt1 = firstExisting (dirs , "crt1.o" )
@@ -316,18 +402,40 @@ func findLinuxDynamicLinker() string {
316402 return override
317403 }
318404
319- var candidates []string
320- switch runtime .GOARCH {
321- case "amd64" :
322- candidates = []string {"ld-linux-x86-64.so.2" , "ld-musl-x86_64.so.1" }
323- case "arm64" :
324- candidates = []string {"ld-linux-aarch64.so.1" , "ld-musl-aarch64.so.1" }
325- case "386" :
326- candidates = []string {"ld-linux.so.2" , "ld-musl-i386.so.1" }
327- default :
328- candidates = []string {"ld-linux.so.2" , "ld-musl.so.1" }
405+ candidates := linuxLoaderCandidates ()
406+
407+ // First, check toolchain/lib directory (bundled files from bootstrap)
408+ if tcLib := toolchainLibDir (); tcLib != "" {
409+ for _ , name := range candidates {
410+ path := filepath .Join (tcLib , name )
411+ if utilsfs .IsValidFile (path ) {
412+ return path
413+ }
414+ }
415+ }
416+
417+ // Then, use gcc to dynamically find the loader
418+ cc := resolveCCompiler ()
419+ if cc != "" {
420+ for _ , name := range candidates {
421+ if path := gccPrintFile (cc , name ); path != "" {
422+ return path
423+ }
424+ }
425+ // Also check gcc library directories
426+ if dirs := gccLibDirs (cc ); len (dirs ) > 0 {
427+ for _ , dir := range dirs {
428+ for _ , name := range candidates {
429+ path := filepath .Join (dir , name )
430+ if utilsfs .IsValidFile (path ) {
431+ return path
432+ }
433+ }
434+ }
435+ }
329436 }
330437
438+ // Last resort: check hardcoded paths
331439 dirs := linuxLibDirs ()
332440 for _ , dir := range dirs {
333441 for _ , name := range candidates {
@@ -342,19 +450,30 @@ func findLinuxDynamicLinker() string {
342450}
343451
344452func linuxLibDirs () []string {
345- dirs := []string {"/lib" , "/usr/lib" , "/lib64" , "/usr/lib64" }
453+ dirs := []string {}
454+ // Highest priority: toolchain/lib directory (bundled files from bootstrap)
346455 if tc := toolchainLibDir (); tc != "" {
347- dirs = append ([] string { tc }, dirs ... )
456+ dirs = append (dirs , tc )
348457 }
458+ // Next: use gcc's library directories if available (more dynamic, works across distros)
459+ if cc := resolveCCompiler (); cc != "" {
460+ if gccDirs := gccLibDirs (cc ); len (gccDirs ) > 0 {
461+ dirs = append (dirs , gccDirs ... )
462+ }
463+ }
464+ // Fallback: common system library directories
465+ dirs = append (dirs , "/lib" , "/usr/lib" , "/lib64" , "/usr/lib64" )
466+ // Last resort: distro-specific paths (these won't exist on all distros like Arch)
349467 switch runtime .GOARCH {
350468 case "amd64" :
351- dirs = append ([] string { "/lib/x86_64-linux-gnu" , "/usr/lib/x86_64-linux-gnu" }, dirs ... )
469+ dirs = append (dirs , "/lib/x86_64-linux-gnu" , "/usr/lib/x86_64-linux-gnu" )
352470 case "arm64" :
353- dirs = append ([] string { "/lib/aarch64-linux-gnu" , "/usr/lib/aarch64-linux-gnu" }, dirs ... )
471+ dirs = append (dirs , "/lib/aarch64-linux-gnu" , "/usr/lib/aarch64-linux-gnu" )
354472 case "386" :
355- dirs = append ([] string { "/lib/i386-linux-gnu" , "/usr/lib/i386-linux-gnu" }, dirs ... )
473+ dirs = append (dirs , "/lib/i386-linux-gnu" , "/usr/lib/i386-linux-gnu" )
356474 }
357475
476+ // Remove duplicates while preserving order
358477 seen := make (map [string ]struct {}, len (dirs ))
359478 unique := make ([]string , 0 , len (dirs ))
360479 for _ , dir := range dirs {
@@ -389,6 +508,46 @@ func findWindowsCrtObjects() (string, string, string) {
389508 crtbegin := os .Getenv ("FERRET_LD_CRTBEGIN" )
390509 crtend := os .Getenv ("FERRET_LD_CRTEND" )
391510
511+ // First, check toolchain/lib directory (bundled files from bootstrap)
512+ if tcLib := toolchainLibDir (); tcLib != "" {
513+ if crt2 == "" {
514+ if path := filepath .Join (tcLib , "crt2.o" ); utilsfs .IsValidFile (path ) {
515+ crt2 = path
516+ }
517+ }
518+ if crtbegin == "" {
519+ if path := filepath .Join (tcLib , "crtbegin.o" ); utilsfs .IsValidFile (path ) {
520+ crtbegin = path
521+ }
522+ }
523+ if crtend == "" {
524+ if path := filepath .Join (tcLib , "crtend.o" ); utilsfs .IsValidFile (path ) {
525+ crtend = path
526+ }
527+ }
528+ }
529+
530+ // If still missing, use gcc to dynamically find files
531+ cc := resolveCCompiler ()
532+ if cc != "" {
533+ if crt2 == "" {
534+ if path := gccPrintFile (cc , "crt2.o" ); path != "" {
535+ crt2 = path
536+ }
537+ }
538+ if crtbegin == "" {
539+ if path := gccPrintFile (cc , "crtbegin.o" ); path != "" {
540+ crtbegin = path
541+ }
542+ }
543+ if crtend == "" {
544+ if path := gccPrintFile (cc , "crtend.o" ); path != "" {
545+ crtend = path
546+ }
547+ }
548+ }
549+
550+ // Last resort: check toolchain/lib directory via windowsLibDirs
392551 dirs := windowsLibDirs ()
393552 if crt2 == "" {
394553 crt2 = firstExisting (dirs , "crt2.o" )
@@ -422,6 +581,15 @@ func containsArg(args []string, value string) bool {
422581 return false
423582}
424583
584+ func containsLibDir (flags []string , dir string ) bool {
585+ for i , flag := range flags {
586+ if flag == "-L" && i + 1 < len (flags ) && flags [i + 1 ] == dir {
587+ return true
588+ }
589+ }
590+ return false
591+ }
592+
425593func resolveToolchainPath () string {
426594 if override := os .Getenv ("FERRET_TOOLCHAIN_PATH" ); override != "" {
427595 if utilsfs .IsDir (override ) {
@@ -499,3 +667,91 @@ func withPathEnv(env []string, key, dir string) []string {
499667 }
500668 return append (env , key + "=" + dir )
501669}
670+
671+ func resolveCCompiler () string {
672+ if val := os .Getenv ("FERRET_CC" ); val != "" {
673+ if path , err := exec .LookPath (val ); err == nil {
674+ return path
675+ }
676+ }
677+ if val := os .Getenv ("CC" ); val != "" {
678+ if path , err := exec .LookPath (val ); err == nil {
679+ return path
680+ }
681+ }
682+ if runtime .GOOS == "darwin" {
683+ if path , err := exec .LookPath ("clang" ); err == nil {
684+ return path
685+ }
686+ }
687+ if path , err := exec .LookPath ("gcc" ); err == nil {
688+ return path
689+ }
690+ return ""
691+ }
692+
693+ func gccPrintFile (cc , name string ) string {
694+ if cc == "" {
695+ return ""
696+ }
697+ out , err := exec .Command (cc , "-print-file-name=" + name ).CombinedOutput ()
698+ if err != nil {
699+ return ""
700+ }
701+ path := strings .TrimSpace (string (out ))
702+ if path == "" || path == name {
703+ return ""
704+ }
705+ if ! utilsfs .IsValidFile (path ) {
706+ return ""
707+ }
708+ return path
709+ }
710+
711+ func gccLibDirs (cc string ) []string {
712+ if cc == "" {
713+ return nil
714+ }
715+ out , err := exec .Command (cc , "-print-search-dirs" ).CombinedOutput ()
716+ if err != nil {
717+ return nil
718+ }
719+ lines := strings .Split (string (out ), "\n " )
720+ for _ , line := range lines {
721+ line = strings .TrimSpace (line )
722+ if ! strings .HasPrefix (line , "libraries:" ) {
723+ continue
724+ }
725+ parts := strings .SplitN (line , "=" , 2 )
726+ if len (parts ) != 2 {
727+ continue
728+ }
729+ raw := strings .TrimSpace (parts [1 ])
730+ if raw == "" {
731+ continue
732+ }
733+ chunks := strings .Split (raw , string (os .PathListSeparator ))
734+ dirs := make ([]string , 0 , len (chunks ))
735+ for _ , chunk := range chunks {
736+ if chunk == "" {
737+ continue
738+ }
739+ dirs = append (dirs , chunk )
740+ }
741+ return dirs
742+ }
743+ return nil
744+ }
745+
746+ func linuxLoaderCandidates () []string {
747+ switch runtime .GOARCH {
748+ case "amd64" :
749+ return []string {"ld-linux-x86-64.so.2" , "ld-musl-x86_64.so.1" }
750+ case "arm64" :
751+ return []string {"ld-linux-aarch64.so.1" , "ld-musl-aarch64.so.1" }
752+ case "386" :
753+ return []string {"ld-linux.so.2" , "ld-musl-i386.so.1" }
754+ default :
755+ return []string {"ld-linux.so.2" , "ld-musl.so.1" }
756+ }
757+ }
0 commit comments