package Module::Install::Rust;

use 5.006;
use strict;
use warnings;

use Module::Install::Base;
use TOML 0.97 ();
use Config ();

our @ISA = qw( Module::Install::Base );

=head1 NAME

Module::Install::Rust - Helpers to build Perl extensions written in Rust

=head1 VERSION

Version 0.02

=cut

our $VERSION = '0.04';


=head1 SYNOPSIS

    # In Makefile.PL
    use inc::Module::Install;

    # ...

    rust_requires libc => "0.2";
    rust_write;

    WriteAll;

=head1 DESCRIPTION

This package allows L<Module::Install> to build Perl extensions written in Rust.

=head1 COMMANDS

=head2 rust_requires

    rust_requires libc => "0.2";
    rust_requires internal_crate => { path => "../internal_crate" };

This command is used to specify Rust dependencies. First argument should be a
crate name, second - either a version string, or a hashref with keys per Cargo
manifest spec.

=cut

sub rust_requires {
    my ($self, $name, $spec) = @_;
    $self->{rust_requires}{$name} = $spec;
}

=head2 rust_feature

    rust_feature default => [ "some_feature" ];
    rust_feature some_feature => [ "some-crate/feature" ];

This command adds items to C<[features]> section of the generated C<Cargo.toml>.

=cut

sub rust_feature {
    my ($self, $name, $spec) = @_;
    die "Feature $name is already defined" if $self->{rust_features}{$name};
    $self->{rust_features}{$name} = $spec;
}

=head2 rust_profile

    rust_profile debug => { "opt-level" => 1 };
    rust_profile release => { lto => 1 };

This command configures a C<[profile]> section to the generated C<Cargo.toml>.

=cut

sub rust_profile {
    my ($self, $name, $spec) = @_;
    die "Profile $name is already configured" if $self->{rust_profile}{$name};

    $self->{rust_profile}{$name} = $spec;
}

=head2 rust_use_perl_xs

    rust_use_perl_xs;

Configure crate to use C<perl-xs> bindings.

=cut

sub rust_use_perl_xs {
    my ($self, $spec) = @_;

    $spec //= { version => "0" };

    $self->rust_requires("perl-xs", $spec);
    $self->rust_clean_on_rebuild("perl-sys");
}

=head2 rust_clean_on_rebuild

    rust_clean_on_rebuild;
    # or
    rust_clean_on_rebuild qw/crate_name/;

If Makefile changed since last build, force C<cargo clean> run. If crate names
are specified, force clean only for those packages (C<cargo clean -p>).

=cut

sub rust_clean_on_rebuild {
    my ($self, @args) = @_;

    my $crates = $self->{cargo_clean} //= [];
    push @$crates, @args;
}

=head2 rust_write

    rust_write;

Writes C<Cargo.toml> and sets up Makefile options as needed.

=cut

sub rust_write {
    my $self = shift;

    $self->_rust_write_cargo;
    $self->_rust_setup_makefile;
}

sub _rust_crate_name {
    lc shift->name
}

sub _rust_target_name {
    shift->_rust_crate_name =~ s/-/_/gr
}

sub _rust_write_cargo {
    my $self = shift;

    my $crate_spec = {
        package => {
            name => $self->_rust_crate_name,
            description => $self->abstract,
            version => "1.0.0", # FIXME
        },

        lib => {
            "crate-type" => [ "cdylib" ],
        },
    };

    $crate_spec->{dependencies} = $self->{rust_requires}
        if $self->{rust_requires};

    $crate_spec->{features} = $self->{rust_features}
        if $self->{rust_features};

    $crate_spec->{profile} = $self->{rust_profile}
        if $self->{rust_profile};

    open my $f, ">", "Cargo.toml" or die $!;
    $f->print("# This file is autogenerated\n\n");
    $f->print(TOML::to_toml($crate_spec));
    close $f or die $!;
}

sub _rust_setup_makefile {
    my $self = shift;
    my $class = ref $self;

    # FIXME: don't assume libraries have "lib" prefix
    my $libname = "lib" . $self->_rust_target_name;

    my $rustc_opts = "";
    my $postproc;
    if ($^O eq "darwin") {
        # Linker flag to allow bundle to use symbols from the parent process.
        $rustc_opts = "-C link-args='-undefined dynamic_lookup'";

        # On darwin, Perl uses special darwin-specific format for loadable
        # modules. Normally it is produced by passing "-bundle" flag to the
        # linker, but Rust as of 1.12 does not support that.
        #
        # "-C link-args=-bundle" doesn't work, because then "-bundle" conflicts
        # with "-dylib" option used by rustc.
        #
        # However, it seems possible to produce correct ".bundle" file by
        # running linker with correct options on the shared library that was
        # created by rustc.
        $postproc = <<MAKE;
	\$(LD) \$(LDDLFLAGS) -o \$@ \$<
MAKE
    } else {
        $postproc = <<MAKE;
	\$(CP) \$< \$@
MAKE
    }



    $self->postamble(<<MAKE);
# --- $class section:

INST_RUSTDYLIB = \$(INST_ARCHAUTODIR)/\$(DLBASE).\$(DLEXT)
RUST_TARGETDIR = target/release
RUST_DYLIB = \$(RUST_TARGETDIR)/$libname.\$(SO)
CARGO = cargo
CARGO_OPTS = --release
RUSTC_OPTS = $rustc_opts

dynamic :: \$(INST_RUSTDYLIB)

MAKE

    if ($self->{cargo_clean}) {
        my @opts = map qq{-p "$_"}, @{$self->{cargo_clean}};

        $self->postamble(<<MAKE);
\$(RUST_DYLIB) ::
	test \$(FIRST_MAKEFILE) -ot \$@ || \$(CARGO) clean \$(CARGO_OPTS) @opts

MAKE
    }

    $self->postamble(<<MAKE);
\$(RUST_DYLIB) ::
	PERL=\$(FULLPERL) \$(CARGO) rustc \$(CARGO_OPTS) -- \$(RUSTC_OPTS)

\$(INST_RUSTDYLIB): \$(RUST_DYLIB)
$postproc

clean ::
	\$(CARGO) clean
	\$(RM) Cargo.toml Cargo.lock
MAKE
}

=head1 AUTHOR

Vickenty Fesunov, C<< <kent at setattr.net> >>

=head1 BUGS

Please report any bugs or feature requests to L<https://github.com/vickenty/mi-rust>.

=head1 LICENSE AND COPYRIGHT

Copyright 2016 Vickenty Fesunov.

This module may be used, modified, and distributed under the same terms as Perl
itself. Please see the license that came with your Perl distribution for
details.

=cut

1;