Affix
view release on metacpan or search on metacpan
lib/Affix/Build.pm view on Meta::CPAN
package Affix::Build v1.0.9 {
use v5.40;
use experimental qw[class try];
use Config;
use Path::Tiny;
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 };
}
lib/Affix/Build.pm view on Meta::CPAN
}
# 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 {
( run in 0.955 second using v1.01-cache-2.11-cpan-39bf76dae61 )