Affix

 view release on metacpan or  search on metacpan

lib/Affix/Build.pm  view on Meta::CPAN

            @cxxflags = map { chomp; $_ } grep { defined && length } Text::ParseWords::parse_line( q/ /, 1, $flags->{cxxflags} // '' );
            @ldflags  = map { chomp; $_ } grep { defined && length } Text::ParseWords::parse_line( q/ /, 1, $flags->{ldflags}  // '' );
        }

        method add ( $input, %args ) {
            $_lib = ();                        # Reset cached library handle
            my ( $path, $lang );
            if ( ref $input eq 'SCALAR' ) {    # Inline source code
                $args{lang} // croak q[Parameter 'lang' (extension) is required for inline source];
                $lang = lc $args{lang};
                croak "Invalid language '$lang'" unless $lang =~ /^[a-z0-9+#]+$/;

                # Generate a unique filename in the build dir
                state $counter = 0;
                my $fname = sprintf( "source_%03d.%s", ++$counter, $lang );
                $path = $build_dir->child($fname);
                $path->spew_utf8($$input);
            }
            else {                             #  File path
                $path = Path::Tiny::path($input)->absolute;
                croak "File not found: $path" unless $path->exists;
                ($lang) = $path =~ /\.([^.]+)$/;
                $lang = lc( $lang // '' );
            }

            # Handle local flags
            my $local_flags = $args{flags} // [];
            $local_flags = [ split ' ', $local_flags ] unless builtin::reftype $local_flags eq 'ARRAY';
            push @sources, { path => $path, lang => $lang, flags => $local_flags };
        }

        method compile_and_link () {
            croak "No sources added" unless @sources;

            # Check if we are mixing languages
            my %langs = map { $_->{lang} => 1 } @sources;
            if ( ( scalar keys %langs ) > 1 ) {
                return $self->_strategy_polyglot();
            }
            return $_lib = $self->_strategy_native();
        }
        method link { $_lib //= $self->compile_and_link(); $_lib }

        # Used when only one language is present. We delegate the entire build process
        # to that language's compiler to produce the final shared library.
        method _strategy_native () {
            my $src     = $sources[0];
            my $l       = $src->{lang};
            my $handler = $self->_resolve_handler($l);
            return $self->$handler( $src, $libname, 'dynamic' );
        }

        # Used when mixing languages (e.g. C + Rust). We compile everything to
        # static artifacts (.o or .a) and then use the system linker to combine them.
        method _strategy_polyglot () {
            my ( @files, @libs );
            foreach my $src (@sources) {
                my $handler = $self->_resolve_handler( $src->{lang} );

                # Request 'static' output from the handler
                my $res = $self->$handler( $src, undef, 'static' );
                push @files, $res->{file};
                push @libs,  @{ $res->{libs} } if $res->{libs};
            }

            # Link step
            my @cmd = ($linker);
            push @cmd, $os eq 'MSWin32' ? ('-shared') : ( '-shared', '-fPIC' );
            push @cmd, '-Wl,--export-all-symbols' if $os eq 'MSWin32' && $linker =~ /gcc|g\+\+|clang/;
            push @cmd, '-o', $libname->stringify;

            # MinGW Static Lib Fix: --whole-archive ensures unused symbols (like language runtimes) are kept
            my $is_gcc = ( $linker =~ /gcc|g\+\+|clang/ || $Config{cc} =~ /gcc/ );
            foreach my $f (@files) {
                my $p = "$f";
                if ( $is_gcc && $p =~ /\Q$Config{_a}\E$/ ) {
                    push @cmd, '-Wl,--whole-archive', $p, '-Wl,--no-whole-archive';
                }
                else {
                    push @cmd, $p;
                }
            }
            my %seen;
            for my $l (@libs) {
                next if $seen{$l}++;
                push @cmd, "-l$l";
            }
            push @cmd, @ldflags;
            $self->_run(@cmd);
            return $libname;
        }

        # Helper to map extensions to method names
        method _resolve_handler ($l) {

            # Normalize
            if    ( $l =~ /^(cpp|cxx|cc|c\+\+)$/ )      { return '_build_cpp'; }
            elsif ( $l =~ /^(f|f90|f95|for|fortran)$/ ) { return '_build_fortran'; }
            elsif ( $l =~ /^(ru?st?)$/ )                { return '_build_rust'; }
            elsif ( $l =~ /^(cs|csharp|c#)$/ )          { return '_build_csharp'; }
            elsif ( $l =~ /^(fs|fsharp|f#)$/ )          { return '_build_fsharp'; }
            elsif ( $l =~ /^(s|asm|assembly)$/ )        { return '_build_asm'; }
            elsif ( $l =~ /^(adb|ads|ada)$/ )           { return '_build_ada'; }
            elsif ( $l =~ /^(hs|lhs|haskell)$/ )        { return '_build_haskell'; }
            elsif ( $l =~ /^(cr|crystal)$/ )            { return '_build_crystal'; }
            elsif ( $l =~ /^(fut|futhark)$/ )           { return '_build_futhark'; }
            elsif ( $l =~ /^(pas|pp|pascal)$/ )         { return '_build_pascal'; }
            elsif ( $l =~ /^(cbl|cob|cobol)$/ )         { return '_build_cobol'; }
            elsif ( $l =~ /^(ml|ocaml)$/ )              { return '_build_ocaml'; }
            elsif ( $l =~ /^(e|eiffel)$/ )              { return '_build_eiffel'; }
            elsif ( $l =~ /^(go)$/ )                    { return '_build_go'; }
            elsif ( $l =~ /^(zig)$/ )                   { return '_build_zig'; }
            elsif ( $l =~ /^(odin)$/ )                  { return '_build_odin'; }
            elsif ( $l =~ /^(nim)$/ )                   { return '_build_nim'; }
            elsif ( $l =~ /^(d|dlang)$/ )               { return '_build_d'; }
            elsif ( $l =~ /^(swift)$/ )                 { return '_build_swift'; }
            elsif ( $l =~ /^(v|vlang)$/ )               { return '_build_v'; }

            # Fallback to C
            return '_build_c';
        }

        method _run (@cmd) {
            print STDERR "[Affix] Exec: @cmd\n" if $debug;

            # use Data::Dump;
            # ddx \@cmd;
            my ( $stdout, $stderr, $exit ) = capture {
                system @cmd;
            };
            if ( !!$exit ) {
                warn $stdout if $stdout;
                warn $stderr if $stderr;
                my $rc = $exit >> 8;
                croak "Command failed (Exit $rc): @cmd";
            }
        }

        method _can_run (@cmd) {
            for my $c (@cmd) {
                return $c if MM->maybe_command($c);
                for my $dir ( File::Spec->path ) {
                    my $abs = File::Spec->catfile( $dir, $c );
                    return $abs if MM->maybe_command($abs);
                }
            }
            return undef;
        }
        method _base ($file) { return $file->basename(qr/\.[^.]+$/); }
        #
        method _build_c ( $src, $out, $mode ) {
            my $file  = $src->{path};
            my @local = @{ $src->{flags} };
            my $cc    = $Config{cc} // 'cc';
            if ( $mode eq 'dynamic' ) {

                # Combine Global CFLAGS + Local Flags + Global LDFLAGS
                my @cmd = ( $cc, '-shared', @cflags, @local, "$file", '-o', "$out", @ldflags );
                push @cmd, '-fPIC' unless $os eq 'MSWin32';
                $self->_run(@cmd);
                return $out;
            }
            else {
                my $obj = $build_dir->child( $self->_base($file) . $Config{_o} );

                # Combine Global CFLAGS + Local Flags
                my @cmd = ( $cc, '-c', @cflags, @local, "$file", '-o', "$obj" );
                push @cmd, '-fPIC' unless $os eq 'MSWin32';
                $self->_run(@cmd);
                return { file => $obj };
            }
        }

        method _build_cpp ( $src, $out, $mode ) {
            my $file  = $src->{path};
            my @local = @{ $src->{flags} };
            my $cxx   = ( $Config{cc} =~ /gcc/ ) ? 'g++' : ( ( $Config{cc} =~ /clang/ ) ? 'clang++' : 'c++' );
            if ( $mode eq 'dynamic' ) {
                my @cmd = ( $cxx, '-shared', @cxxflags, @local, "$file", '-o', "$out", @ldflags );
                push @cmd, '-fPIC' unless $os eq 'MSWin32';
                $self->_run(@cmd);
                return $out;
            }
            else {
                my $obj = $build_dir->child( $self->_base($file) . $Config{_o} );
                my @cmd = ( $cxx, '-c', @cxxflags, @local, "$file", '-o', "$obj" );
                push @cmd, '-fPIC' unless $os eq 'MSWin32';
                $self->_run(@cmd);
                return { file => $obj };
            }
        }

        #~ https://fasterthanli.me/series/making-our-own-executable-packer/part-5
        #~ https://stackoverflow.com/questions/71704813/writing-and-linking-shared-libraries-in-assembly-32-bit
        #~ https://github.com/therealdreg/nasm_linux_x86_64_pure_sharedlib
        method _build_asm ( $src, $out, $mode ) {
            my $file  = $src->{path};
            my @local = @{ $src->{flags} };
            my $obj   = $build_dir->child( $self->_base($file) . $Config{_o} );

            # Detect Assembler Type: .asm (Intel/NASM) vs .s (AT&T/CC)
            my $is_nasm  = ( $file =~ /\.asm$/i );
            my $compiled = 0;
            if ($is_nasm) {    # .asm = Intel syntax = NASM
                my $nasm = $self->_can_run('nasm') // croak "NASM not found";
                my $fmt  = $os eq 'MSWin32' ? 'win64' : ( $os eq 'darwin' ? 'macho64' : 'elf64' );
                $self->_run( $nasm, '-f', $fmt, @local, "$file", '-o', "$obj" );

lib/Affix/Build.pm  view on Meta::CPAN

            my $file  = $src->{path};
            my @local = @{ $src->{flags} };
            my $zig   = $self->_can_run('zig') // croak "Zig not found";
            if ( $mode eq 'dynamic' ) {
                $self->_run( $zig, 'build-lib', '-dynamic', @local, "$file", "-femit-bin=$out" );
                return $out;
            }
            else {
                my $lib = $build_dir->child( $self->_base($file) . $Config{_a} );
                $self->_run( $zig, 'build-lib', '-static', @local, "$file", "-femit-bin=$lib" );
                return { file => $lib, libs => ( $os eq 'MSWin32' ? ['ntdll'] : [] ) };
            }
        }

        method _build_fortran ( $src, $out, $mode ) {
            my $file  = $src->{path};
            my @local = @{ $src->{flags} };
            my $fc    = $self->_can_run(qw[gfortran ifx ifort]) // croak "No Fortran compiler";
            if ( $mode eq 'dynamic' ) {
                my @cmd = ( $fc, '-shared', @local, "$file", '-o', "$out", @ldflags );
                push @cmd, '-fPIC' unless $os eq 'MSWin32';
                $self->_run(@cmd);
                return $out;
            }
            else {
                my $obj = $build_dir->child( $self->_base($file) . $Config{_o} );
                my @cmd = ( $fc, '-c', @local, "$file", '-o', "$obj" );
                push @cmd, '-fPIC' unless $os eq 'MSWin32';
                $self->_run(@cmd);
                return { file => $obj, libs => ( $os eq 'MSWin32' ? [] : ['gfortran'] ) };
            }
        }

        #~ https://peterme.net/dynamic-libraries-in-nim.html
        method _build_nim ( $src, $out, $mode ) {
            my $file = $src->{path};
            my $nim  = $self->_can_run('nim') // croak "Nim not found";
            if ( $mode eq 'dynamic' ) {
                $self->_run( $nim, 'c', '--app:lib', '--noMain', '--cc:c', "--out:$out", "$file" );
                return $out;
            }
            else {
                my $lib = $build_dir->child( $self->_base($file) . $Config{_a} );
                $self->_run( $nim, 'c', '--app:staticlib', '--noMain', '--cc:c', '--nimcache:' . $build_dir, "--out:$lib", "$file" );
                return { file => $lib };
            }
        }

        #~ https://www.rangakrish.com/index.php/2023/04/02/building-v-language-dll/
        #~ https://dev.to/piterweb/how-to-create-and-use-dlls-on-vlang-1p13
        method _build_v ( $src, $out, $mode ) {
            my $file = $src->{path};
            my $v    = $self->_can_run('v') // croak "V not found";
            if ( $mode eq 'dynamic' ) {
                $self->_run( $v, '-shared', '-o', "$out", "$file" );
                return $out;
            }
            else {
                my $c_file = $build_dir->child( $self->_base($file) . '.c' );
                $self->_run( $v, '-o', "$c_file", "$file" );
                return $self->_build_c( { path => $c_file }, undef, 'static' );
            }
        }

        #~ swiftc point.swift -emit-module -emit-library
        #~ https://forums.swift.org/t/creating-a-c-accessible-shared-library-in-swift/45329/5
        #~ https://theswiftdev.com/building-static-and-dynamic-swift-libraries-using-the-swift-compiler/#should-i-choose-dynamic-or-static-linking
        method _build_swift ( $src, $out, $mode ) {
            my $file = $src->{path};
            my $sc   = $self->_can_run('swiftc') // croak "Swiftc not found";

            # Swiftc uses -emit-library for both, just differs on output filename usually
            # But for static we usually want -static if available or just .a
            if ( $mode eq 'dynamic' ) {
                $self->_run( $sc, '-emit-library', "$file", '-o', "$out" );
                return $out;
            }
            else {
                my $lib = $build_dir->child( $self->_base($file) . $Config{_a} );

                # Note: Swift static linking is complex on Linux, but -emit-library -static usually works
                $self->_run( $sc, '-emit-library', '-static', '-parse-as-library', "$file", '-o', "$lib" );
                return { file => $lib };
            }
        }

        #~ https://gcc.gnu.org/onlinedocs/gcc-3.4.0/gnat_ug_unx/Creating-an-Ada-Library.html
        method _build_ada ( $src, $out, $mode ) {
            my $file = $src->{path};
            my $gnat = $self->_can_run('gnatmake') // croak "GNAT not found";

            # Ada compilation is usually to Object first
            my $obj = $build_dir->child( $self->_base($file) . $Config{_o} );
            my @cmd = ( $gnat, '-c', '-u', "$file" );
            push @cmd, '-fPIC' unless $os eq 'MSWin32';

            # GNAT often outputs to CWD
            $self->_run(@cmd);
            my $cwd_obj = Path::Tiny::path( $self->_base($file) . $Config{_o} );
            $cwd_obj->move($obj) if $cwd_obj->exists && $cwd_obj->absolute ne $obj->absolute;
            if ( $mode eq 'dynamic' ) {

                # Link the object to shared
                # We use the generic linker logic for this single file
                my $linker = $Config{cc} // 'cc';
                $self->_run( $linker, '-shared', '-o', "$out", "$obj" );
                return $out;
            }
            return { file => $obj, libs => ['gnat'] };
        }

        #~ https://github.com/bennoleslie/haskell-shared-example
        #~ https://www.hobson.space/posts/haskell-foreign-library/
        method _build_haskell ( $src, $out, $mode ) {
            my $file = $src->{path};
            my $ghc  = $self->_can_run('ghc') // croak "GHC not found";
            if ( $mode eq 'dynamic' ) {
                my @cmd = ( $ghc, '-shared', '-dynamic', "$file", '-o', "$out" );
                push @cmd, '-fPIC' unless $os eq 'MSWin32';
                $self->_run(@cmd);
                return $out;



( run in 1.356 second using v1.01-cache-2.11-cpan-d80b1682f3f )