#!/usr/bin/perl
#
# dpkg-genbuildinfo
#
# Copyright © 1996 Ian Jackson
# Copyright © 2000,2001 Wichert Akkerman
# Copyright © 2003-2013 Yann Dirson <dirson@debian.org>
# Copyright © 2006-2016 Guillem Jover <guillem@debian.org>
# Copyright © 2014 Niko Tyni <ntyni@debian.org>
# Copyright © 2014-2015 Jérémy Bobbio <lunar@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

use v5.36;

use List::Util qw(any);
use Cwd;
use File::Basename;
use File::Temp;
use POSIX qw(:fcntl_h :locale_h strftime);

use Dpkg ();
use Dpkg::Gettext;
use Dpkg::Checksums;
use Dpkg::ErrorHandling;
use Dpkg::IPC;
use Dpkg::Path qw(find_command);
use Dpkg::Arch qw(
    get_build_arch
    get_host_arch
    debarch_eq debarch_to_gnutriplet
);
use Dpkg::BuildTypes;
use Dpkg::BuildOptions;
use Dpkg::BuildFlags;
use Dpkg::BuildProfiles qw(get_build_profiles);
use Dpkg::BuildInfo qw(get_build_env_allowed);
use Dpkg::Control::Info;
use Dpkg::Control::Fields;
use Dpkg::Control;
use Dpkg::Changelog::Parse;
use Dpkg::Deps;
use Dpkg::Dist::Files;
use Dpkg::Lock;
use Dpkg::Version;
use Dpkg::Vendor qw(get_current_vendor run_vendor_hook);

textdomain('dpkg-dev');

my $controlfile = 'debian/control';
my $changelogfile = 'debian/changelog';
my $changelogformat;
my $fileslistfile = 'debian/files';
my $uploadfilesdir = '..';
my $outputfile;
my $stdout = 0;
my $admindir = $Dpkg::ADMINDIR;
my %use_feature = (
    kernel => 0,
    path => 0,
);
my @build_profiles = get_build_profiles();
my $buildinfo_format = '1.0';
my $buildinfo;

my $checksums = Dpkg::Checksums->new();
my %distbinaries;
my %archadded;
my @archvalues;

sub get_build_date {
    my $date;

    setlocale(LC_TIME, 'C');
    $date = strftime('%a, %d %b %Y %T %z', localtime);
    setlocale(LC_TIME, '');

    return $date;
}

# There is almost the same function in dpkg-checkbuilddeps, they probably
# should be factored out.
sub parse_status {
    my $status = shift;

    my $facts = Dpkg::Deps::KnownFacts->new();
    my %depends;
    my @essential_pkgs;

    local $/ = '';
    open my $status_fh, '<', $status or syserr(g_('cannot open %s'), $status);
    while (<$status_fh>) {
        next unless /^Status: .*ok installed$/m;

        my ($package) = /^Package: (.*)$/m;
        my ($version) = /^Version: (.*)$/m;
        my ($arch) = /^Architecture: (.*)$/m;
        my ($multiarch) = /^Multi-Arch: (.*)$/m;

        $facts->add_installed_package($package, $version, $arch, $multiarch);

        if (/^Essential: yes$/m) {
            push @essential_pkgs, $package;
        }

        if (/^Provides: (.*)$/m) {
            my $provides = deps_parse($1,
                reduce_arch => 1,
                virtual => 1,
                union => 1,
            );

            next if not defined $provides;

            deps_iterate($provides, sub {
                my $dep = shift;
                $facts->add_provided_package($dep->{package}, $dep->{relation},
                                             $dep->{version}, $package);
            });
        }

        foreach my $deptype (qw(Pre-Depends Depends)) {
            next unless /^$deptype: (.*)$/m;

            my $depends = $1;
            foreach (split /,\s*/, $depends) {
                push @{$depends{"$package:$arch"}}, $_;
            }
        }
    }
    close $status_fh;

    return ($facts, \%depends, \@essential_pkgs);
}

sub append_deps {
    my ($pkgs, @deps) = @_;

    foreach my $dep_str (@deps) {
        next unless $dep_str;

        my $deps = deps_parse($dep_str,
            reduce_restrictions => 1,
            build_dep => 1,
            build_profiles => \@build_profiles,
        );

        # We add every sub-dependencies as we cannot know which package in
        # an OR dependency has been effectively used.
        deps_iterate($deps, sub {
            my $pkg = shift;

            push @{$pkgs},
                $pkg->{package} . (defined $pkg->{archqual} ? ':' . $pkg->{archqual} : '');
            1
        });
    }
}

sub collect_installed_builddeps {
    my $control = shift;

    my ($facts, $depends, $essential_pkgs) = parse_status("$admindir/status");
    my %seen_pkgs;
    my @unprocessed_pkgs;

    # Parse essential packages list.
    append_deps(\@unprocessed_pkgs,
                @{$essential_pkgs},
                run_vendor_hook('builtin-build-depends'),
                $control->get_source->{'Build-Depends'});

    if (build_has_any(BUILD_ARCH_DEP)) {
        append_deps(\@unprocessed_pkgs,
                    $control->get_source->{'Build-Depends-Arch'});
    }

    if (build_has_any(BUILD_ARCH_INDEP)) {
        append_deps(\@unprocessed_pkgs,
                    $control->get_source->{'Build-Depends-Indep'});
    }

    my $installed_deps = Dpkg::Deps::AND->new();

    while (my $pkg_name = shift @unprocessed_pkgs) {
        next if $seen_pkgs{$pkg_name};
        $seen_pkgs{$pkg_name} = 1;

        my $required_architecture;
        if ($pkg_name =~ /\A(.*):(.*)\z/) {
            $pkg_name = $1;
            my $arch = $2;
            $required_architecture = $arch if $arch !~ /\A(?:all|any|native)\Z/
        }
        my $pkg;
        my $qualified_pkg_name;
        foreach my $installed_pkg (@{$facts->{pkg}->{$pkg_name}}) {
            if (! defined $required_architecture ||
                $required_architecture eq $installed_pkg->{architecture}) {
                $pkg = $installed_pkg;
                $qualified_pkg_name = $pkg_name . ':' . $installed_pkg->{architecture};
                last;
            }
        }
        if (defined $pkg) {
            my $version = $pkg->{version};
            my $architecture = $pkg->{architecture};
            my $new_deps_str = defined $depends->{$qualified_pkg_name} ? deps_concat(@{$depends->{$qualified_pkg_name}}) : '';
            my $new_deps = deps_parse($new_deps_str);
            if (! defined $required_architecture) {
                $installed_deps->add(Dpkg::Deps::Simple->new("$pkg_name (= $version)"));
            } else {
                $installed_deps->add(Dpkg::Deps::Simple->new("$qualified_pkg_name (= $version)"));

                # Dependencies of foreign packages are also foreign packages
                # (or Arch:all) so we need to qualify them as well. We figure
                # out if the package is actually foreign by searching for an
                # installed package of the right architecture.
                deps_iterate($new_deps, sub {
                    my $dep = shift;
                    return unless defined $facts->{pkg}->{$dep->{package}};
                    $dep->{archqual} //= $architecture
                        if any { $_[0]->{architecture} eq $architecture }, @{$facts->{pkg}->{$dep->{package}}};
                    1;
                });
            }

            # We add every sub-dependencies as we cannot know which package
            # in an OR dependency has been effectively used.
            deps_iterate($new_deps, sub {
                push @unprocessed_pkgs,
                     $_[0]->{package} . (defined $_[0]->{archqual} ? ':' . $_[0]->{archqual} : '');
                1
            });
        } elsif (defined $facts->{virtualpkg}->{$pkg_name}) {
            # Virtual package: we cannot know for sure which implementation
            # is the one that has been used, so let us add them all.
            foreach my $provided (@{$facts->{virtualpkg}->{$pkg_name}}) {
                push @unprocessed_pkgs, $provided->{provider};
            }
        }
        # Else: it is a package in an OR dependency that has been otherwise
        # satisfied.
    }
    $installed_deps->simplify_deps(Dpkg::Deps::KnownFacts->new());
    $installed_deps->sort();
    $installed_deps = "\n" . $installed_deps->output();
    $installed_deps =~ s/, /,\n/g;

    return $installed_deps;
}

sub is_cross_executable {
    my $host_arch = get_host_arch();
    my $build_arch = get_build_arch();

    return if $host_arch eq $build_arch;

    # If we are cross-compiling, record whether it was possible to execute
    # the host architecture by cross-compiling and executing a small
    # host-arch binary.
    my $CC = debarch_to_gnutriplet($host_arch) . '-gcc';

    # If we do not have a cross-compiler, we might be in the process of
    # building one or cross-compiling using a language other than C/C++,
    # and aborting the build is then not very useful.
    return if ! find_command($CC);

    my $crossprog = <<~'CROSSPROG';
        #include <unistd.h>
        int main() { write(1, "ok", 2); return 0; }
    CROSSPROG
    my ($stdout, $stderr) = ('', '');
    my $tmpfh = File::Temp->new();
    spawn(
        exec => [ $CC, '-w', '-x', 'c', '-o', $tmpfh->filename, '-' ],
        from_string => \$crossprog,
        to_string => \$stdout,
        error_to_string => \$stderr,
        wait_child => 1,
        no_check => 1,
    );
    if ($?) {
        print { *STDOUT } $stdout;
        print { *STDERR } $stderr;
        eval {
            subprocerr("$CC -w -x c -");
        };
        warning($@);
        return;
    }
    close $tmpfh;
    spawn(
        exec => [ $tmpfh->filename ],
        error_to_file => '/dev/null',
        to_string => \$stdout,
        wait_child => 1,
        no_check => 1,
    );

    return 1 if $? == 0 && $stdout eq 'ok';
    return 0;
}

sub get_build_tainted_by {
    my @tainted = run_vendor_hook('build-tainted-by');

    if (is_cross_executable()) {
        push @tainted, 'can-execute-cross-built-programs';
    }

    return @tainted;
}

sub cleansed_environment {
    # Consider only allowed variables which are not supposed to leak
    # local user information.
    my %env = map {
        $_ => $ENV{$_}
    } grep {
        exists $ENV{$_}
    } get_build_env_allowed();

    # Record flags from dpkg-buildflags.
    my $bf = Dpkg::BuildFlags->new();
    $bf->load_system_config();
    $bf->load_user_config();
    $bf->load_environment_config();
    foreach my $flag ($bf->list()) {
        next if $bf->get_origin($flag) eq 'vendor';

        # We do not need to record *_{STRIP,APPEND,PREPEND} as they
        # have been used already to compute the above value.
        $env{"DEB_${flag}_SET"} = $bf->get($flag);
    }

    return join "\n", map { $_ . '="' . ($env{$_} =~ s/"/\\"/gr) . '"' }
                      sort keys %env;
}

sub version {
    printf g_("Debian %s version %s.\n"), $Dpkg::PROGNAME, $Dpkg::PROGVERSION;

    printf g_('
This is free software; see the GNU General Public License version 2 or
later for copying conditions. There is NO warranty.
');
}

sub usage {
    printf g_(
'Usage: %s [<option>...]')
    . "\n\n" . g_(
"Options:
  --build=<type>[,...]     specify the build <type>: full, source, binary,
                             any, all (default is \'full\').
  -c<control-file>         get control info from this file.
  -l<changelog-file>       get per-version info from this file.
  -f<files-list-file>      get .deb files list from this file.
  -F<changelog-format>     force changelog format.
  -O[<buildinfo-file>]     write to stdout (or <buildinfo-file>).
  -u<upload-files-dir>     directory with files (default is '..').
  --always-include-kernel  always include Build-Kernel-Version.
  --always-include-path    always include Build-Path.
  --admindir=<directory>   change the administrative directory.
  -?, --help               show this help message.
      --version            show the version.
"), $Dpkg::PROGNAME;
}

my $build_opts = Dpkg::BuildOptions->new();
$build_opts->parse_features('buildinfo', \%use_feature);

while (@ARGV) {
    $_ = shift @ARGV ;
    if (m/^--build=(.*)$/) {
        set_build_type_from_options($1, $_);
    } elsif (m/^-c(.*)$/) {
        $controlfile = $1;
    } elsif (m/^-l(.*)$/) {
        $changelogfile = $1;
    } elsif (m/^-f(.*)$/) {
        $fileslistfile = $1;
    } elsif (m/^-F([0-9a-z]+)$/) {
        $changelogformat = $1;
    } elsif (m/^-u(.*)$/) {
        $uploadfilesdir = $1;
    } elsif (m/^-O$/) {
        $stdout = 1;
    } elsif (m/^-O(.*)$/) {
        $outputfile = $1;
    } elsif (m/^(--buildinfo-id)=.*$/) {
        # Deprecated option.
        warning(g_('%s is deprecated; it is without effect'), $1);
    } elsif (m/^--always-include-kernel$/) {
        $use_feature{kernel} = 1;
    } elsif (m/^--always-include-path$/) {
        $use_feature{path} = 1;
    } elsif (m/^--admindir=(.*)$/) {
        $admindir = $1;
    } elsif (m/^-(?:\?|-help)$/) {
        usage();
        exit(0);
    } elsif (m/^--version$/) {
        version();
        exit(0);
    } else {
        usageerr(g_("unknown option '%s'"), $_);
    }
}

my $control = Dpkg::Control::Info->new($controlfile);
my $fields = Dpkg::Control->new(type => CTRL_FILE_BUILDINFO);
my $dist = Dpkg::Dist::Files->new();

# Retrieve info from the current changelog entry.
my %changelog_opts = (
    filename => $changelogfile,
);
$changelog_opts{changelogformat} = $changelogformat if $changelogformat;
my $changelog = changelog_parse(%changelog_opts);

# Retrieve info from the former changelog entry to handle binNMUs.
$changelog_opts{count} = 1;
$changelog_opts{offset} = 1;
my $prev_changelog = changelog_parse(%changelog_opts);

my $sourceversion = Dpkg::Version->new($changelog->{'Binary-Only'} ?
                    $prev_changelog->{'Version'} : $changelog->{'Version'});
my $binaryversion = Dpkg::Version->new($changelog->{'Version'});

# Include .dsc if available.
my $spackage = $changelog->{'Source'};
my $sversion = $sourceversion->as_string(omit_epoch => 1);

if (build_has_any(BUILD_SOURCE)) {
    my $dsc = "${spackage}_${sversion}.dsc";

    $checksums->add_from_file("$uploadfilesdir/$dsc", key => $dsc);

    push @archvalues, 'source';
}

my $dist_count = 0;

$dist_count = $dist->load($fileslistfile) if -e $fileslistfile;

if (build_has_any(BUILD_BINARY)) {
    error(g_('binary build with no binary artifacts found; .buildinfo is meaningless'))
        if $dist_count == 0;

    foreach my $file ($dist->get_files()) {
        # Make us a bit idempotent.
        next if $file->{filename} =~ m/\.buildinfo$/;

        if (defined $file->{arch}) {
            my $arch_all = debarch_eq('all', $file->{arch});

            next if build_has_none(BUILD_ARCH_INDEP) and $arch_all;
            next if build_has_none(BUILD_ARCH_DEP) and not $arch_all;

            $distbinaries{$file->{package}} = 1 if defined $file->{package};
        }

        my $path = "$uploadfilesdir/$file->{filename}";
        $checksums->add_from_file($path, key => $file->{filename});

        if (defined $file->{package_type} and $file->{package_type} =~ m/^u?deb$/) {
            push @archvalues, $file->{arch}
                if defined $file->{arch} and not $archadded{$file->{arch}}++;
        }
    }
}

$fields->{'Format'} = $buildinfo_format;
$fields->{'Source'} = $spackage;
$fields->{'Binary'} = join(' ', sort keys %distbinaries);
# Avoid overly long line by splitting over multiple lines.
if (length($fields->{'Binary'}) > 980) {
    $fields->{'Binary'} =~ s/(.{0,980}) /$1\n/g;
}

$fields->{'Architecture'} = join ' ', sort @archvalues;
$fields->{'Version'} = $binaryversion;

if ($changelog->{'Binary-Only'}) {
    $fields->{'Source'} .= ' (' . $sourceversion . ')';
    $fields->{'Binary-Only-Changes'} =
        $changelog->{'Changes'} . "\n\n"
        . ' -- ' . $changelog->{'Maintainer'}
        . '  ' . $changelog->{'Date'};
}

$fields->{'Build-Origin'} = get_current_vendor();
$fields->{'Build-Architecture'} = get_build_arch();
$fields->{'Build-Date'} = get_build_date();

if ($use_feature{kernel}) {
    my ($kern_rel, $kern_ver);

    ((undef) x 2, $kern_rel, $kern_ver, undef) = POSIX::uname();
    $fields->{'Build-Kernel-Version'} = "$kern_rel $kern_ver";
}

my $cwd = getcwd();
if ($use_feature{path}) {
    $fields->{'Build-Path'} = $cwd;
} else {
    # Only include the build path if its root path is considered acceptable
    # by the vendor.
    foreach my $root_path (run_vendor_hook('builtin-system-build-paths')) {
        if (index($cwd, $root_path) == 0) {
            $fields->{'Build-Path'} = $cwd;
            last;
        }
    }
}

$fields->{'Build-Tainted-By'} = "\n" . join "\n", get_build_tainted_by();

$checksums->export_to_control($fields);

$fields->{'Installed-Build-Depends'} = collect_installed_builddeps($control);

$fields->{'Environment'} = "\n" . cleansed_environment();

# Generate the buildinfo filename.
if ($stdout) {
    # Nothing to do.
} elsif (defined $outputfile) {
    $buildinfo = basename($outputfile);
} else {
    my $arch;

    if (build_has_any(BUILD_ARCH_DEP)) {
        $arch = get_host_arch();
    } elsif (build_has_any(BUILD_ARCH_INDEP)) {
        $arch = 'all';
    } elsif (build_has_any(BUILD_SOURCE)) {
        $arch = 'source';
    }

    my $bversion = $binaryversion->as_string(omit_epoch => 1);
    $buildinfo = "${spackage}_${bversion}_${arch}.buildinfo";
    $outputfile = "$uploadfilesdir/$buildinfo";
}

# Write out the generated .buildinfo file.

if ($stdout) {
    $fields->output(\*STDOUT);
} else {
    my %fileprop;
    foreach my $f (qw(Section Priority)) {
        $fileprop{lc $f} = $control->get_source->{$f} || field_get_default_value($f);
    }

    # Obtain a lock on debian/control to avoid simultaneous updates
    # of debian/files when parallel building is in use.
    my $lockfh;
    my $lockfile = 'debian/control';
    $lockfile = $controlfile if not -e $lockfile;

    sysopen $lockfh, $lockfile, O_WRONLY
        or syserr(g_('cannot write %s'), $lockfile);
    file_lock($lockfh, $lockfile);

    $dist = Dpkg::Dist::Files->new();
    $dist->load($fileslistfile) if -e $fileslistfile;

    foreach my $file ($dist->get_files()) {
        if (defined $file->{package} &&
            $file->{package} eq $spackage &&
            $file->{package_type} eq 'buildinfo' &&
            (debarch_eq($file->{arch}, $fields->{'Architecture'}) ||
             debarch_eq($file->{arch}, 'all') ||
             debarch_eq($file->{arch}, 'source'))) {
            $dist->del_file($file->{filename});
        }
    }

    $dist->add_file($buildinfo, @fileprop{qw(section priority)});
    $dist->save("$fileslistfile.new");

    rename "$fileslistfile.new", $fileslistfile
        or syserr(g_('install new files list file'));

    # Release the lock.
    close $lockfh or syserr(g_('cannot close %s'), $lockfile);

    $fields->save("$outputfile.new");

    rename "$outputfile.new", $outputfile
        or syserr(g_("cannot install output buildinfo file '%s'"), $outputfile);
}
