Affix

 view release on metacpan or  search on metacpan

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

    use File::Spec;
    use Carp          qw[croak];
    use Capture::Tiny qw[capture];
    use ExtUtils::MakeMaker;
    use Text::ParseWords;

    class Affix::Build {

        # Public Parameters
        field $os        : param : reader //= $^O;
        field $clean     : param : reader //= 0;
        field $build_dir : param : reader //= Path::Tiny->tempdir( CLEANUP => $clean );
        field $name      : param : reader //= 'affix_lib';
        field $debug     : param : reader //= 0;
        field $version   : param : reader //= ();

        # Global flags applied to all compilations of that type
        # cflags, cxxflags, ldflags, rustflags, etc.
        field $flags : param : reader //= {};

        # Internal State
        field @sources;
        field $libname : reader;
        field $linker  : reader;

        # Cached Flag Arrays
        field @cflags;
        field @cxxflags;
        field @ldflags;
        field $_lib;
        #
        ADJUST {
            my $so_ext = $Config{so} // 'so';
            $build_dir = Path::Tiny->new($build_dir) unless builtin::blessed $build_dir;

            # Standard convention: Windows DLLs don't need 'lib' prefix, Unix SOs do.
            my $prefix = ( $os eq 'MSWin32' || $name =~ /^lib/ ) ? ''          : 'lib';
            my $suffix = defined $version                        ? ".$version" : '';
            $libname = $build_dir->child("$prefix$name.$so_ext$suffix")->absolute;

            # We prefer C++ drivers (g++, clang++) to handle standard libraries for mixed code (C+Rust, C+C++)
            $linker = $self->_can_run(qw[g++ clang++ c++ icpx]) || $self->_can_run(qw[cc gcc clang icx cl]) || 'c++';

            # Parse global flags...
            @cflags   = map { chomp; $_ } grep { defined && length } Text::ParseWords::parse_line( q/ /, 1, $flags->{cflags}   // '' );
            @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};

                # 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';

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


        #~ https://odin-lang.org/news/calling-odin-from-python/
        #~ https://odin-lang.org/docs/install/#release-requirements--notes
        method _build_odin ( $src, $out, $mode ) {
            my $file  = $src->{path};
            my @local = @{ $src->{flags} };
            my $odin  = $self->_can_run('odin') // croak "Odin not found";
            if ( $mode eq 'dynamic' ) {
                $self->_run( $odin, 'build', "$file", '-file', '-build-mode:dll', @local, "-out:$out" );
                return $out;
            }
            else {
                my $obj = $build_dir->child( $self->_base($file) . $Config{_o} );
                my @cmd = ( $odin, 'build', "$file", '-file', '-build-mode:obj', @local, "-out:$obj" );
                push @cmd, '-reloc-mode:pic' unless $os eq 'MSWin32';
                $self->_run(@cmd);
                unless ( $obj->exists ) {    # Attempt to find it if Odin misnamed it (e.g. .obj vs .o)
                    my $cwd_obj = Path::Tiny::path( $self->_base($file) . $Config{_o} );
                    $cwd_obj->move($obj) if $cwd_obj->exists;
                }
                return { file => $obj };
            }
        }

        #~ https://dlang.org/articles/dll-linux.html#dso9
        #~ dmd -c dll.d -fPIC
        #~ dmd -oflibdll.so dll.o -shared -defaultlib=libphobos2.so -L-rpath=/path/to/where/shared/library/is
        method _build_d ( $src, $out, $mode ) {
            my $file = $src->{path};
            my $dmd  = $self->_can_run(qw[dmd ldc2 gdc]) // croak "D compiler not found";
            if ( $mode eq 'dynamic' ) {
                my @cmd = ( $dmd, '-shared', "$file", "-of=$out" );
                push @cmd, '-fPIC' unless $os eq 'MSWin32';
                $self->_run(@cmd);
                return $out;
            }
            else {
                my $lib = $build_dir->child( $self->_base($file) . $Config{_a} );
                my @cmd = ( $dmd, '-lib', "$file", "-of=$lib" );
                push @cmd, '-fPIC' unless $os eq 'MSWin32';
                $self->_run(@cmd);
                return { file => $lib };
            }
        }
        method _build_csharp ( $file, $out, $mode ) { $self->_build_dotnet( $file, $out, $mode, 'cs' ); }

        #~ https://github.com/secana/Native-FSharp-Library
        #~ https://secanablog.wordpress.com/2020/02/01/writing-a-native-library-in-f-which-can-be-called-from-c/
        method _build_fsharp ( $file, $out, $mode ) { $self->_build_dotnet( $file, $out, $mode, 'fs' ); }

        method _build_dotnet ( $src, $out, $mode, $lang ) {
            my $file     = $src->{path};
            my $dotnet   = $self->_can_run('dotnet') // croak "Dotnet not found";
            my $proj_dir = $build_dir->child( "dotnet_${lang}_" . $self->_base($file) );
            $proj_dir->mkpath;
            $file->copy( $proj_dir->child( $file->basename ) );
            my $ext      = $lang eq 'fs' ? 'fsproj' : 'csproj';
            my $proj     = $proj_dir->child("Build.$ext");
            my $lib_type = ( $mode eq 'dynamic' ) ? 'Shared'                                               : 'Static';
            my $items    = $lang eq 'fs'          ? '<ItemGroup><Compile Include="**/*.fs" /></ItemGroup>' : '';
            $proj->spew_utf8(<<"XML");
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
<NativeLib>$lib_type</NativeLib>
<SelfContained>true</SelfContained>
</PropertyGroup>
$items
</Project>
XML
            my $out_dir = $proj_dir->child('out');
            my $rid     = $os eq 'MSWin32' ? 'win-x64' : 'linux-x64';

            # Local flags? Dotnet CLI args are tricky, assuming they aremsbuild props?
            # Ignoring for now to keep it safe, or pass as raw args if user knows what they do.
            my @local = @{ $src->{flags} };
            $self->_run( "$dotnet", 'publish', "$proj", '-r', $rid, '-o', "$out_dir", @local );
            if ( $mode eq 'dynamic' ) {
                my $dll_ext = $Config{so};
                $dll_ext = ".$dll_ext" unless $dll_ext =~ /^\./;
                my ($artifact) = grep {/\Q$dll_ext\E$/} $out_dir->children;
                croak "Dotnet build failed" unless $artifact;
                Path::Tiny::path($artifact)->move($out);
                return $out;
            }
            else {
                my $lib_ext = $Config{_a};
                my ($artifact) = grep {/\Q$lib_ext\E$/} $out_dir->children;
                croak "Dotnet build failed" unless $artifact;
                return { file => Path::Tiny::path($artifact) };
            }
        }

        #~ https://ziglang.org/documentation/0.13.0/#Exporting-a-C-Library
        #~ zig build-lib mathtest.zig -dynamic
        method _build_zig ( $src, $out, $mode ) {
            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;
            }



( run in 1.386 second using v1.01-cache-2.11-cpan-39bf76dae61 )