# debhelper buildsystem for Rust crates using Cargo
#
# This builds Debian rust crates to be installed into a system-level
# crate registry in /usr/share/cargo/registry containing crates that
# can be used and Build-Depended upon by other Debian packages. The
# debcargo(1) tool will automatically generate Debian source packages
# that uses this buildsystem and packagers are not expected to use this
# directly which is why the documentation is poor.
#
# If you have a multi-language program such as firefox or librsvg that
# includes private Rust crates or libraries not exposed to others, you
# should instead use our cargo wrapper, in /usr/share/cargo/bin/cargo,
# which this script also uses. That file contains usage instructions.
# You then should define a Build-Depends on cargo and not dh-cargo.
# The Debian cargo package itself also uses the wrapper as part of its
# own build, which you can look at for a real usage example.
#
# Josh Triplett <josh@joshtriplett.org>
# Ximin Luo <infinity0@debian.org>

package Debian::Debhelper::Buildsystem::cargo;

use strict;
use warnings;
use Cwd;
use Debian::Debhelper::Dh_Lib;
use Dpkg::BuildFlags;
use Dpkg::Changelog::Debian;
use Dpkg::Control::Info;
use Dpkg::Version;
use JSON::PP;
use base 'Debian::Debhelper::Buildsystem';

sub DESCRIPTION {
    "Rust Cargo"
}

sub set_extra_flags {
    my $bf = Dpkg::BuildFlags->new();
    my @cargo_flags = ();
    $bf->load_config();
    if ($bf->has_features("optimize") and {$bf->get_features("optimize")}->{"lto"}) {
        push(@cargo_flags, '--config', 'profile.release.lto = "thin"');
    }
    return @cargo_flags;
}

sub cargo_version {
    my $src = shift;
    open(F, "cargo metadata --manifest-path $src --no-deps --format-version 1 |");
    local $/;
    my $json = JSON::PP->new;
    my $manifest = $json->decode(<F>);
    return %{@{%{$manifest}{'packages'}}[0]}{'version'} . "";
}

sub deb_host_rust_type {
    open(F, 'printf "include /usr/share/rustc/architecture.mk\n\
all:\n\
	echo \$(DEB_HOST_RUST_TYPE)\n\
" | make --no-print-directory -sf - |');
    $_ = <F>;
    chomp;
    return $_;
}

sub check_auto_buildable {
    my $this = shift;
    if (-f $this->get_sourcepath("Cargo.toml")) {
        return 1;
    }
    return 0;
}

sub new {
    my $class = shift;
    my $this = $class->SUPER::new(@_);
    $this->enforce_in_source_building();
    return $this;
}

sub pre_building_step {
    my $this = shift;
    my $step = shift;

    # Many files are coming from crates.io with incorrect timestamp
    # See https://github.com/rust-lang/crates.io/issues/3859
    complex_doit("find . ! -newermt 'jan 01, 2000' -exec touch -d@" . $ENV{SOURCE_DATE_EPOCH} . " {} +");

    $this->{cargo_home} = Cwd::abs_path("debian/cargo_home");
    $this->{host_rust_type} = deb_host_rust_type;

    my $control = Dpkg::Control::Info->new();

    my $source = $control->get_source();
    my $crate = $source->{'X-Cargo-Crate'};
    if (!$crate) {
        $crate = $source->{Source};
        $crate =~ s/^ru[sz]t-//;
        $crate =~ s/-[0-9]+(\.[0-9]+)*$//;
    }
    $this->{crate} = $crate;
    my $changelog = Dpkg::Changelog::Debian->new(range => { count => 1 });
    $changelog->load("debian/changelog");
    my $version = $source->{'X-Cargo-Crate-Version'};
    if (!$version) {
	$version = Dpkg::Version->new(@{$changelog}[0]->get_version())->version();
    }
    $this->{version} = $version;
    my @packages = $control->get_packages();
    $this->{libpkg} = 0;
    $this->{binpkg} = 0;
    $this->{featurepkg} = [];
    foreach my $package (@packages) {
        my $pkg = $package->{Package};

        # handle -N<package>
        next if !process_pkg($pkg);

        if ($pkg =~ /^libru[sz]t-.*-dev$/) {
            if ($pkg =~ /\+/) {
                push(@{$this->{featurepkg}}, $pkg);
                next;
            }
            if ($this->{libpkg}) {
                error("Multiple Cargo lib packages found: " . $this->{libpkg} . " and " . $pkg);
            }
            $this->{libpkg} = $pkg;
        } elsif ($package->{Architecture} ne 'all' && !$this->{binpkg}) {
            $this->{binpkg} = $pkg;
        }
    }
    if (!$this->{libpkg} && !$this->{binpkg}) {
        error("Could not find any Cargo lib or bin packages following dh-cargo scheme! Check debian/control.");
    }
    if (@{$this->{featurepkg}} && !$this->{libpkg}) {
        error("Found feature packages but no lib package, check debian/control.");
    }

    my $parallel = $this->get_parallel();
    $this->{j} = $parallel > 0 ? ["-j$parallel"] : [];

    $ENV{'CARGO_HOME'} = $this->{cargo_home};
    $ENV{'DEB_CARGO_CRATE'} = $crate . '_' . $this->{version};
    $ENV{'DEB_HOST_RUST_TYPE'} = $this->{host_rust_type};
    $ENV{'DEB_HOST_GNU_TYPE'} = dpkg_architecture_value("DEB_HOST_GNU_TYPE");

    $this->SUPER::pre_building_step($step);
}

sub get_sources {
    my $this=shift;
    opendir(my $dirhandle, '.');
    my $excluded = {
        map { $_ => 1 }
        (
            '.',
            '..',
            '.git',
            '.pc',
            'debian',
            'target',
        )
    };
    my @sources = grep { !$excluded->{$_} } readdir($dirhandle);
    push @sources, 'debian/patches' if -d 'debian/patches';
    closedir($dirhandle);
    @sources
}

sub configure {
    my $this=shift;
    doit("cp", "debian/cargo-checksum.json",
         ".cargo-checksum.json");
    if (defined $ENV{'CARGO_VENDOR_DIR'}) {
        doit("/usr/share/cargo/bin/cargo", "prepare-debian", $ENV{'CARGO_VENDOR_DIR'});
        doit("/usr/share/cargo/bin/dh-cargo-vendored-sources");
    } else {
        rm_files("Cargo.lock");
        doit("/usr/share/cargo/bin/cargo", "prepare-debian", "debian/cargo_registry", "--link-from-system");
    }
}

sub build {
    my $this=shift;

    my $destdir=shift;
    if ($this->{libpkg}) {
        my $copied_sources = generated_file("_source", "copied_sources");
        install_dir($copied_sources);
        my @sources = $this->get_sources();
        doit("cp", "--parents", "-at", $copied_sources, @sources);
        doit("cp", "debian/cargo-checksum.json", "$copied_sources/.cargo-checksum.json");
        # honor -X
        if (defined($dh{EXCLUDE_FIND}) && $dh{EXCLUDE_FIND} ne '') {
                my $find_options = "$dh{EXCLUDE_FIND}";
                complex_doit("find $copied_sources $find_options -delete");
        }
    }
}

sub test {
    my $this=shift;
    my $cmd="build";
    if (!defined $_[0]) {
        # nop
    } elsif ($_[0] eq "test") {
        $cmd="test";
        shift;
    } elsif ($_[0] eq "build") {
        shift;
    }
    # Check that the thing compiles. This might fail if e.g. the package
    # requires non-rust system dependencies and the maintainer didn't provide
    # this additional information to debcargo.
    doit("/usr/share/cargo/bin/cargo", $cmd, @_);
    # test generating Built-Using fields
    doit("env", "CARGO_CHANNEL=debug", "/usr/share/cargo/bin/dh-cargo-built-using");
}

sub install {
    my $this=shift;
    my $destdir=shift;
    my @extra_flags = $this->set_extra_flags();
    if ($this->{libpkg}) {
        require Cwd;
        my $cwd = Cwd::getcwd();
        my $crate = $this->{crate} . '-' . ($this->{version} =~ tr/~/-/r);

        # needs to be absolute because of chdir below
        my $target = $cwd . '/' . tmpdir($this->{libpkg}) . "/usr/share/cargo/registry/$crate";
        install_dir($target);

        my $copied_sources = generated_file("_source", "copied_sources");
        my %options = (
                chdir => $copied_sources,
        );
        doit(\%options, "cp", "--parents", "-at", $target, ".");
        # honor -X
        if (defined($dh{EXCLUDE_FIND}) && $dh{EXCLUDE_FIND} ne '') {
                my $find_options = "$dh{EXCLUDE_FIND}";
                complex_doit("find $target $find_options -delete");
        }
        # prevent an ftpmaster auto-reject regarding files with old dates.
        doit("touch", "-d@" . $ENV{SOURCE_DATE_EPOCH}, "$target/Cargo.toml");
    }
    foreach my $pkg (@{$this->{featurepkg}}) {
        my $target = tmpdir($pkg) . "/usr/share/doc";
        install_dir($target);
        make_symlink_raw_target($this->{libpkg}, "$target/$pkg");
    }
    if ($this->{binpkg}) {
        # Do the install
        my $destdir = $ENV{'DESTDIR'} || tmpdir($this->{binpkg});
        doit("env", "DESTDIR=$destdir",
             "/usr/share/cargo/bin/cargo", "install", @extra_flags, @_);
        if (defined $ENV{'CARGO_VENDOR_DIR'}) {
            if (-e "Cargo.lock") {
                my $lockfile_dir=tmpdir($this->{binpkg}) . "/usr/share/doc/" . $this->{binpkg};
                install_dir($lockfile_dir);
                install_file("Cargo.lock", $lockfile_dir . "/Cargo.lock");
            } else {
                warning("Cargo.lock file not found!");
            }
        }
        # generate Built-Using fields
        doit("/usr/share/cargo/bin/dh-cargo-built-using", $this->{binpkg});
    }
}

sub clean {
    my $this=shift;
    doit("touch", "--no-create", "-d@" . $ENV{SOURCE_DATE_EPOCH}, ".cargo_vcs_info.json");
    doit("/usr/share/cargo/bin/cargo", "clean", @_);
    rm_files(".cargo-checksum.json");
    doit("rm", "-rf", "debian/cargo_registry");
}

1
