# A debhelper build system class for handling pkg-js-autopkgtest test.
#
# Copyright: 2019, Xavier Guimard <yadd@debian.org>
# License: GPL-2+

package Debian::Debhelper::Buildsystem::nodejs;

use strict;
use warnings;
use Debian::Debhelper::Dh_Lib;
use File::Find;
use JSON;
use IPC::Run qw(run);
use parent qw(Debian::Debhelper::Buildsystem);

use constant MAX_BUILD_LOOP => 3;

sub NPMIGNOREDEFAULT {
    $ENV{NPMIGNOREDEFAULT} || '/usr/share/pkg-js-tools/npmignore.default';
}

# echo, touch and ls are here to help tests
my @known_build_commands = (qw(grunt echo touch ls));

#(qw(babel grunt gulp node rollup tsc webpack cat echo touch ls));

# Order is important here, example: tsc must be launched before other
my @knwonBuildFiles = (
    [ 'Gruntfile.js'     => ['grunt'] ],
    [ 'Gruntfile.coffee' => ['grunt'] ],

    #[ 'tsconfig.json'     => ['tsc'] ],
    #[ 'gulpfile.js' => ['gulp'] ],
    #[ 'rollup.config.js'  => [ 'rollup', '-c' ] ],
    #[ 'webpack.config.js' => ['webpack'] ],
    #[ 'webpackfile.js'    => ['webpack'] ],
);

my $knownCommandMap = { babel => 'babeljs', };

sub DEBUG     { $ENV{DEB_BUILD_PKG_JS_DEBUG} }
sub DEBUGFILE { $ENV{DEB_BUILD_PKG_JS_DEBUG_FILE} }

sub DESCRIPTION {
    "pkg-js-tools test";
}

### CONSTANTS

# Regexp compiled from dh_nodejs/dh_root_files.excluded
my $rootFilesIgnored =
qr/^(?:(?:g(?:ulpfile\.(?:babel\.j|(?:t|j))|runt(?:file)?\.j)|(?:rollup[\.\-].*config|karma\.conf|.mocharc)\.j)s|b(?:abel\.config\.js(?:on)?|inding\.gyp|ench.*\.js|ower\.json)|c(?:o(?:mponent\.json|ntribute|pying)|hange(?:log|s?)|akefile)|a(?:ut(?:otest\.watchr|hors?)|va\.config\.js|ppveyor\.yml)|j(?:est\.config\.js|sl\.node\.conf)|.*\.(?:m(?:arkdown|d)|pdf|txt)|t(?:sconfig\.json|ests?\.js)|l(?:icen[cs]e.*|erna\.json)|\.(?:babelrc(?:.js)?|.*)|(?:Docker|make)file|package-lock\.json|yarn\.lock|history)$/i;
my $filesIgnored =
qr/^(?:\.(?:(?:wafpickle-|eslint|_).*|npm(?:ignore|rc)|lock-wscript|gitignore)|.*\.(?:c(?:offee|\+\+|cp?)?|orig|def|mk|h)|(?:[jm]|binding.M)akefile|readme(?:\.(?:txt|md))?|package-lock\.json|npm-debug\.log|yarn\.lock)$/i;

# Regexp compiled from dh_nodejs/dh_dirs.excluded
my $dirIgnored =
qr{^(?:(.*/)?(?:\.(?:_.*|git|svn|hg)|.(?:DS_Store|deps)|__[^/]*__|fixtures?|CVS)|(?:archived-packag|node_modul)es|(?:example|test|doc)s?|bench|\..*)(?:/.*)?}i;

# Extensions that overrides $rootFilesIgnored if set in package.json#files
my $authorizedExt = qr/\.(?:js)$/;

### PRIVATE METHODS

sub open_file {
    my ( $self, $file ) = @_;
    error unless $file;
    my $f;
    open( $f, $file ) or error $!;
    my @lines = map {
        chomp;
        s/#.*$//;
        s/^\s*(.*?)\s*/$1/;
        $_ ? $_ : ();
    } <$f>;
    close $f;
    return @lines;
}

my ( %json, $main_p );

sub main_package {
    my ($self) = @_;
    return ( $main_p ? $main_p : () ) if defined $main_p;
    return '.' unless -e 'debian/nodejs/main';
    my @lines = $self->open_file('debian/nodejs/main');
    error 'Malformed debian/nodejs/main'       if @lines > 1;
    warning "Missing main directory $lines[0]" if @lines and !-d $lines[0];
    $main_p = $lines[0] || '';
    return @lines;
}

# package.json cache
sub pjson {
    my ( $self, $dir ) = @_;
    unless ( -e "$dir/package.json" ) {
        warning "/!\\ $dir/package.json not found\n";
        return undef;
    }
    return $json{$dir} if $json{$dir};
    my $pkgjson;
    open $pkgjson, "$dir/package.json";
    my $content = join '', <$pkgjson>;
    close $pkgjson;
    $json{$dir} = JSON::from_json($content);
    if ( -e "debian/nodejs/$dir/name" ) {
        $json{$dir}->{name} =
          [ $self->open_file("debian/nodejs/$dir/name") ]->[0];
    }
    return $json{$dir};
}

sub component_list {
    my $self = shift;
    my ( @components, %packages );
    if ( -e 'debian/watch' ) {
        map { push @components, $1 if (/component=([\w\-\.]+)/) }
          $self->open_file('debian/watch');
    }
    if ( -e 'debian/nodejs/additional_components' ) {
        print "Found debian/nodejs/additional_components\n";
        map {
            print 'Adding component(s): ' . join( ', ', glob($_) ) . "\n";
            push @components, glob($_)
        } $self->open_file('debian/nodejs/additional_components');
    }
    foreach my $component (@components) {
        next if $self->main_package and $component eq $self->main_package;
        if ( -d $component ) {
            my $package;
            eval { $package = $self->pjson($component)->{name} };
            if ($@) {
                warning "Unable to load $component: $@";
                next;
            }
            $packages{$component} = $package;
        }
        else {
            warning "Can't find $component directory in " . `pwd`;
            next;
        }
    }
    return \%packages;
}

sub pattern {
    my ( $self, $line ) = @_;
    my ( $p, $pattern );
    print "Parsing expression $line\n";
    $line =~ m/^(.*?)([\{\*].*)$/ or error "Bad line $line";
    my $tmp = $line;
    my ( $dir, $expr ) = ( $1, $2 );
    if ($dir) {
        $dir =~ s#/+([^/]*)$##;
        $expr = "$1$expr";
        $p    = $dir;
    }
    else {
        $p = $self->main_package;
    }
    my @dirs = split( m#\*\*/#, $expr );
    @dirs = map {
        my $s = quotemeta($_);
        $s =~ s#\\\*#[^/]*#g;
        while ( $s =~ s/\\\{(.*?)\\\}/_____/ ) {
            my $tmp = '(?:' . join( '|', split( /\\?,/, $1 ) ) . ')';
            $s =~ s/_____/$tmp/;
        }
        $s
    } @dirs;
    my $pat = "^$p/" . join( '.*(?<=/)', @dirs ) . '$';
    print "Line $tmp becomes: $pat\n";
    eval { $pattern = qr/$pat/ };
    error << "EOF" if ($@);

Unable to parse expression: "$tmp" (converted into "$pat")
If it is correct, please fill a bug against pkg-js-tools.
To workaround, you can overwrite "files" field in debian/nodejs. See
/usr/share/doc/pkg-js-tools/README.md.

EOF
    return ( $p, $pattern );
}

# Private method to build modules
sub build_module {
    my ( $self, $dir ) = @_;
    my $buildCommand;

    # Instead of an override, maintainers can write a debian/nodejs/build
    # or debian/nodejs/<component>/build
    my $builddir = $self->get_buildpath . "/$dir";
    my @count    = ( $dir =~ m#(/)#g );

    if ( -e "debian/nodejs/$dir/build" ) {
        print "Found debian/nodejs/$dir/build\n";
        print_and_doit(
            { chdir => $builddir },
            'sh', '-ex',
            (
                ( $dir ne '.' ? '../' x ( scalar(@count) + 1 ) : '' )
                . "debian/nodejs/$dir/build"
            )
        );
        return 1;
    }
    $buildCommand = $self->pjson($dir)->{scripts}->{build};
    my @commands;
    if ($buildCommand) {
        @commands = $self->resolve_command( $dir, $buildCommand );
    }
    else {
        print "No build command found, searching known files\n";
        foreach my $i (@knwonBuildFiles) {
            if ( -e $i->[0] ) {
                push @commands, $i->[1];
            }
        }
    }
    my $failures = 0;
    my $success  = 0;
    foreach my $cmd (@commands) {

        #$self->_generic_doit_in_dir( $dir, \&print_and_doit, @$_ );
        my $res = print_and_doit_noerror( { chdir => $builddir }, @$cmd );
        if ($res) {
            print 'Command "' . join( ' ', @$cmd ) . qq'" succeeded in $dir\n';
            $success++;
        }
        else {
            warning '### Command "'
              . join( ' ', @$cmd )
              . qq'" failed in $dir\n';
            $failures++;
        }
    }
    if ( $failures and not $success ) {
        warning <<EOF;

############################################################################
$failures failures, unable to build automatically.
Install a debian/nodejs/$dir/build or add a "override_dh_auto_build:" target
in debian/rules
############################################################################
EOF
    }
    return $failures ? $success : 1;
}

sub resolve_command {
    my ( $self, $dir, $command, $count ) = @_;
    $count //= 0;
    if ( $count > MAX_BUILD_LOOP ) {
        warning "Max loop command exceed, aborting";
        return ();
    }
    my @tmp = split /\s*&&\s*/, $command;
    my @commands;
    foreach my $inst (@tmp) {
        my ( $cmd, @rargs ) = split /\s+/, $inst;
        my @args;
        while ( my $c = shift @rargs ) {
            if ( $c =~ s/^(["'])// ) {
                my $sep = $1;
                while ( not( $c =~ s/$sep$// ) and @rargs ) {
                    $c = "$c " . shift(@rargs);
                }
            }
            push @args, $c;
        }
        if ( $cmd eq 'npm' and $args[0] and $args[0] eq 'run' ) {
            my $target = $self->pjson($dir)->{scripts}->{ $args[1] };
            if ( $target and $target ne $command ) {
                $count++;
                push @commands, $self->resolve_command( $dir, $target, $count );
            }
            next;
        }
        elsif ( grep { $cmd eq $_ } @known_build_commands ) {
            $cmd = $knownCommandMap->{$cmd} if $knownCommandMap->{$cmd};
            push @commands, [ $cmd, @args ];
        }
    }
    return @commands;
}

sub dolink {
    my ( $self, $src, $dest ) = @_;
    return if $src eq $dest;
    my $dir = $dest;
    $self->doit_in_builddir( 'mkdir', '-p', $dir )
      if $dir =~ s#/[^/]*$##
      and
      not doit_noerror( { chdir => $self->get_buildpath }, 'test', '-e', $dir );
    my @count = ( $dir =~ m#(/)#g );
    $src = ( '../' x ( scalar(@count) + 1 ) ) . $src unless $src =~ m#^/#;
    $self->doit_in_builddir( 'ln', '-s', $src, $dest );
}

sub do_unlink {
    my ( $self, $link ) = @_;
    unlink $link;
    while ( $link =~ s#/[^/]*?$## ) {
        rmdir $link;
    }
}

# Private method to install modules: search for package.json#files field, if
# not found, install any files except ignored by regexp
sub install_module {
    my ( $self, $destdir, $archPath, $path, $dir, @excludedDirs ) = @_;
    my $re = (
        @excludedDirs
        ? '^(?:' . join( '|', @excludedDirs ) . ')(?:/.*)?$'
        : '##'
    );
    $re = qr/$re/;

    # If debian/nodejs/$dir/install or debian/nodejs/$dir/files or
    # "package.json#files" field exists, only its paths will be examined
    my @files;
    my $noFilesField = 1;
    my $skipFiles    = 1;
    if ( -e "debian/nodejs/$dir/install" ) {
        map {
            s/^\s*(.*?)\s*/$1/;
            next if /^\s*#/;
            my $dest = $path;
            my $src  = $_;
            if (/^(.+?)\s+(.+)$/) {
                $src = $1;
                my $tmp = $2;
                if ( $tmp =~ m{^/} ) {
                    $dest = "$destdir/$tmp";
                }
                else {
                    $dest = "$archPath/$tmp";
                }
            }

            $self->doit_in_builddir( 'mkdir', '-p', $dest )
              unless doit_noerror( { chdir => $self->get_buildpath },
                'test', '-e', $dest );
            $self->doit_in_builddir( 'cp', '--reflink=auto', '-a', "$dir/$src",
                $dest );
        } $self->open_file("debian/nodejs/$dir/install");
        return;
    }
    my $npmignore = -e "$dir/.npmignore" ? "$dir/.npmignore" : NPMIGNOREDEFAULT;
    if ( -e "debian/nodejs/$dir/files" ) {
        @files =
          $self->readFilesField( $dir,
            $self->open_file("debian/nodejs/$dir/files") );
        $noFilesField = 0;
        $skipFiles    = 0;
    }
    elsif ( $self->pjson($dir)->{files} ) {
        @files = $self->readFilesField(
            $dir,
            $self->filesFieldToList( $dir, 'files' ),
            $self->filesFieldToList( $dir, 'typings' ),
            $self->filesFieldToList( $dir, 'types' ),
        );
        if ( $self->pjson($dir)->{types} and not $self->pjson($dir)->{typings} )
        {
            warning qq{# /!\ "types" field should be replaced by "typings"}
              . " in $dir/package.json\nPlease report this bug\n";
        }
        $noFilesField = 0;
        push @files,
          grep { $_ ne "$dir/package.json" } $self->readFilesField(
            $dir,
            ( map { s/^/!/; s/^\!\!//; $_ } ( $self->open_file($npmignore) ) )
          );
    }
    else {
        print qq{No "files" field in $dir/package.json, install all files\n};
        @files = ($dir);
        push @files,
          grep { $_ ne "$dir/package.json" } $self->readFilesField(
            $dir,
            ( map { s/^/!/; s/^\!\!//; $_ } ( $self->open_file($npmignore) ) )
          );
    }
    my @dest;
    my $mainFile =
      $self->toJs( $dir, $self->pjson($dir)->{main} || "index.js" );
    $mainFile =~ s#^(?:\./)+##;
    $mainFile = "$dir/$mainFile";
    $mainFile = "$mainFile/index.js" if -d $mainFile;
    warning "MAIN: $mainFile\n" if (DEBUG);
    $mainFile =~ s#//+#/#g;
    my $foundMain    = 0;
    my $foundPkgJson = 0;

    foreach my $p (@files) {
        my $pattern;
        my $not = 0;
        if ( $p =~ s/\!// ) {
            $not = 1;
        }
        if ( $p =~ m/[\*|\{]/ ) {
            ( $p, $pattern ) = $self->pattern($p);
        }
        unless ( -e $p ) {
            warning "### Missing $p, skipping\n";
            next;
        }
        find(
            sub {
                my $d = $File::Find::dir;
                return if $pattern and "$d/$_" !~ $pattern and $d !~ $pattern;
                $d =~ s#^$dir/?##;

                # Exclusion tests:
                # ----------------
                #
                #  - $skipFiles=1 unless Debian maintainer specified a
                #                 debian/nodejs/install or debian/nodejs/files
                #  - $noFilesField = 1 unless:
                #                      - DM specified a debian/nodejs/install
                #                      - DM specified a debian/nodejs/files
                #                      - package.json contains a "files" field
                my @tests = (

                    # Ignore directories
                    ( -d $_ ),

                    # Don't parse components sub dir for main module
                    ( $d and $d =~ $re and $skipFiles ),

                    # Ignore our debian/ dir
                    (
                              $dir eq '.'
                          and $File::Find::dir =~ m{^(?:./)?debian}
                          and $skipFiles
                    ),

                    # Ignore license, changelog, readme,... files
                    ( $_ =~ $rootFilesIgnored and $noFilesField and !$d ),
                    (
                              $_ =~ $rootFilesIgnored
                          and $skipFiles
                          and ( $noFilesField or $_ !~ $authorizedExt )
                          and !$d
                    ),

          # Ignore Makefile, .eslint anywhere unless DM specified it explicitely
                    ( $_ =~ $filesIgnored and ( $pattern or $p !~ /$_$/ ) ),
                    ( $_ =~ $filesIgnored and ( $pattern or $p !~ /$_$/ ) ),

                  # Ignore test directory unless specified in package.json#files
                    ( $d and $d =~ $dirIgnored and $noFilesField ),

                    # Ignore test.js file unless specified in package.json#files
                    (
                              $File::Find::dir eq $dir
                          and /^(?:example|test)s?\.js$/i
                          and $noFilesField
                    ),
                );

                # Install files unless one exclusion test match
                if ($not) {
                    @dest = grep { $_->[1] ne $File::Find::name } @dest;
                }
                else {
                    unless ( grep { $_ } @tests ) {
                        push @dest, [ $d, $File::Find::name ];
                        $foundMain    = 1 if $File::Find::name eq $mainFile;
                        $foundPkgJson = 1
                          if $File::Find::name eq 'package.json';
                    }

                    # Debug
                    elsif ( DEBUG and not DEBUGFILE ) {
                        warning "# FILE $File::Find::name\n";
                        for ( my $i = 0 ; $i < @tests ; $i++ ) {
                            warning "#     Test$i: "
                              . ( $tests[$i] || 0 ) . "\n";
                        }
                    }
                }
                if ( DEBUGFILE and $_ eq DEBUGFILE ) {
                    warning <<EOF;

# FILE $File::Find::name
#    D:            $d
#    PATH:         $File::Find::dir
#    DIR:          $dir
#    SKIP:         $skipFiles
#    NOFILESFIELD: $noFilesField
EOF
                    warning '# '
                      . ( ( grep { $_ } @tests ) ? 'EXCLUDED' : 'INSTALLED' )
                      . ":\n";
                    for ( my $i = 0 ; $i < @tests ; $i++ ) {
                        warning "#     Test$i: " . ( $tests[$i] || 0 ) . "\n";
                    }
                }
            },
            $p
        );
    }
    unless ( $foundMain or !$self->main_package ) {
        if ( -e $mainFile ) {
            warning
              "package.json#main file not installed, forcing it ($mainFile)\n";
            my $ldir = ( $mainFile =~ m#^(?:$dir/?)?(.*)/# ? $1 : "" );
            push @dest, [ $ldir, $mainFile ];
        }
    }
    unless ($foundPkgJson) {
        push @dest, [ '', "$dir/package.json" ];
    }
    foreach (@dest) {
        $self->doit_in_builddir( 'mkdir', '-p', "$path/$_->[0]" )
          unless doit_noerror( { chdir => $self->get_buildpath },
            'test', '-e', "$path/$_->[0]" );
        my $mode = ( -x $_->[1] ? '755' : '644' );
        $self->doit_in_builddir( 'cp', '--reflink=auto', '-a', $_->[1],
            "$path/$_->[0]/" );
    }
}

sub filesFieldToList {
    my ( $self, $dir, $field ) = @_;
    return () unless $self->pjson($dir)->{$field};
    print qq{Found "$field" field in $dir/package.json, using it\n};
    if ( my $ref = ref $self->pjson($dir)->{$field} ) {
        return ( values %{ $self->pjson($dir)->{$field} } ) if $ref eq 'HASH';
        return @{ $self->pjson($dir)->{$field} };
    }
    return ( "$dir/" . $self->pjson($dir)->{$field} );
}

sub readFilesField {
    my ( $self, $dir, @lines ) = @_;
    my @files = map {
        my $prefix = '';
        $_ = $self->toJs( $dir, $_ );
        my $nowarn;
        if (s/^!//) {
            $prefix = '!';
            $nowarn = 1;
        }
        s#^\.?/##;
        warning "$dir/package.json#files: $_ does not exists\n"
          unless $nowarn
          or ( $_ and -e "$dir/$_" or /[\{\*]/ );
        $_ ? "$prefix$dir/$_" : ();
    } @lines;
    push @files, "$dir/package.json"
      unless grep { $_ eq "$dir/package.json" } @files;
    return @files;
}

sub toJs {
    my ( $self, $dir, $file ) = @_;
    return (
        ( -e "$dir/$file.js" and not -e "$dir/$file" )
        ? "$file.js"
        : "$file"
    );
}

sub cmp_ordered_list {
    my ( $self, $components ) = @_;
    $components //= $self->component_list;
    my @dirs = ( sort keys %$components );
    my @res;
    if ( -e 'debian/nodejs/build_order' ) {
        @res = map {
            my $cmp = $_;
            error "Unknown component $_" unless $components->{$cmp};
            @dirs = grep { $_ ne $cmp } @dirs;
            $cmp;
        } $self->open_file('debian/nodejs/build_order');
    }
    if ( -e 'debian/nodejs/component_links' ) {
        my %lt;
        my %firsts;
        map {
            error "Malformed line $. in debian/nodejs/component_links: $_"
              unless /^([\w\-\.\/]+)\s+([\w\-\.\/]+)$/;
            $lt{$1}     = $2;
            $firsts{$1} = 1;
        } $self->open_file('debian/nodejs/component_links');
        @dirs =
          sort {
                ( $lt{$a} and $lt{$a} eq $b ) ? -1
              : ( $lt{$b} and $lt{$b} eq $a ) ? 1
              : $firsts{$a}                   ? -1
              : $firsts{$b}                   ? 1
              : $a cmp $b
          } @dirs;
    }
    push @res, @dirs;
    return @res;
}

# PUBLIC METHODS
# --------------

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

sub check_auto_buildable {
    my $self = shift;
    if ( -e 'package.json' ) {
        return 1;
    }
    return 0;
}

# auto_configure step: if component are found, create node_modules links
sub configure {
    my $self       = shift;
    my $components = $self->component_list;
    my @dirs       = ( sort keys %$components );
    return
         unless @dirs
      or -e 'debian/nodejs/' . $self->main_package . '/extlinks'
      or -e 'debian/nodejs/' . $self->main_package . '/extcopies'
      or -d 'debian/build_modules' && $self->main_package eq '.';
    $self->doit_in_builddir( 'mkdir', 'node_modules' )
      unless -e 'node_modules';

    # External links
    foreach my $dir (
        (
            $self->main_package
            ? (
                $self->main_package ne '.'
                ? ( $self->main_package, '.' )
                : ('.')
            )
            : ('.')
        ),
        @dirs
      )
    {
        if ( -e "debian/nodejs/$dir/extlinks" ) {
            map {
                my ( $in, $src );
                $_ =~ s/\s+(\S*).*?$//;
                if (
                        $1
                    and $1 eq 'test'
                    and (
                        (
                                $ENV{DEB_BUILD_OPTIONS}
                            and $ENV{DEB_BUILD_OPTIONS} =~ /\bnocheck\b/
                        )
                        or (    $ENV{DEB_BUILD_PROFILES}
                            and $ENV{DEB_BUILD_PROFILES} =~ /\bnocheck\b/ )
                    )
                  )
                {
                    print STDERR "Skipping $_ link\n";
                }
                else {
                    run [ 'nodepath', $_ ], \$in, \$src;
                    if ( $? or not $src ) {
                        print STDERR
                          "### $_ is required by debian/nodejs/$dir/extlinks"
                          . " but not available\n";
                        exit 1;
                    }
                    else {
                        chomp $src;
                        my $tmp = $src;
                        $tmp =~ s#^.*?nodejs/##;
                        $tmp =~ s#/.*?$## or $tmp = '';
                        $tmp = "$tmp/" if $tmp;
                        $self->dolink( $src, "$dir/node_modules/$tmp" );
                    }
                }
            } $self->open_file("debian/nodejs/$dir/extlinks");
        }
        if ( -e "debian/nodejs/$dir/extcopies" ) {
            map {
                my ( $in, $src );
                $_ =~ s/\s+(\S*).*?$//;
                if (
                        $1
                    and $1 eq 'test'
                    and (
                        (
                                $ENV{DEB_BUILD_OPTIONS}
                            and $ENV{DEB_BUILD_OPTIONS} =~ /\bnocheck\b/
                        )
                        or (    $ENV{DEB_BUILD_PROFILES}
                            and $ENV{DEB_BUILD_PROFILES} =~ /\bnocheck\b/ )
                    )
                  )
                {
                    print STDERR "Skipping $_ copy\n";
                }
                else {
                    run [ 'nodepath', $_ ], \$in, \$src;
                    if ( $? or not $src ) {
                        print STDERR
                          "### $_ is required by debian/nodejs/$dir/extcopies"
                          . " but not available\n";
                        exit 1;
                    }
                    else {
                        chomp $src;
                        my $dst = $src;
                        $dst =~ s#.*?nodejs/##;
                        if ( $dst =~ s#/[^/]*$## ) {
                            $self->doit_in_builddir( 'mkdir', '-p',
                                "$dir/node_modules/$dst" );
                        }
                        else {
                            $dst = '';
                        }
                        $self->doit_in_builddir( 'cp', '-rL', $src,
                            "$dir/node_modules/$dst" );
                    }
                }
            } $self->open_file("debian/nodejs/$dir/extcopies");
        }
    }

    # Link each component in node_modules
    foreach my $component (@dirs) {
        my $package = $components->{$component};
        $self->dolink( "$component", "node_modules/$package" )
          unless -e "debian/nodejs/$component/nolink";
    }

    # Links between components
    if ( -e 'debian/nodejs/component_links' ) {
        my %links = map {
            error "Malformed line $. in debian/nodejs/component_links: $_"
              unless /^([\w\-\.\/]+)\s+([\w\-\.\/]+)$/;
            my ( $src, $dest ) = ( $1, $2 );
            foreach my $s ( $src, $dest ) {
                error "component_links: Unknown component $s"
                  unless $components->{$s};
            }
            ( $src => $dest );
        } $self->open_file('debian/nodejs/component_links');
        foreach ( keys %links ) {
            $self->dolink( $_, "$links{$_}/node_modules/$components->{$_}" );
        }
    }

    # Links all modules found in debian/build_modules
    if ( -d 'debian/build_modules' ) {
        opendir my $buildDir, 'debian/build_modules';
        my @buildMods = grep { /^[^\.]/ } ( readdir $buildDir );
        foreach my $mod (@buildMods) {
            if ( $mod =~ /^\@/ ) {
                opendir my $buildDir, "debian/build_modules/$mod";
                my @subMods = grep { /^[^\.]/ } ( readdir $buildDir );
                $self->dolink( "debian/build_modules/$mod/$_",
                    "node_modules/$mod/$_" )
                  foreach (@subMods);
            }
            else {
                $self->dolink( "debian/build_modules/$mod",
                    "node_modules/$mod" );
            }
        }
    }
}

# build step
sub build {
    my $self       = shift;
    my $components = $self->component_list;
    my @dirs       = $self->cmp_ordered_list($components);
    my $res        = 0;
    foreach my $dir ( @dirs, $self->main_package ) {
        $res += $self->build_module($dir);
    }
    unless ($res) {
        warning "Aborting auto_build\n";

        # To avoid hard transition, this step never fail
        #exit 1;
    }
}

# test step: simple require or real test if declared
sub test {
    my $self       = shift;
    my $components = $self->component_list;
    my @dirs       = ( sort keys %$components );
    my @testlinks;
    if ( -d 'debian/tests/test_modules' ) {
        opendir my $testDir, 'debian/tests/test_modules';
        my @testMods = grep { /^[^\.]/ } ( readdir $testDir );
        foreach my $mod (@testMods) {
            if ( $mod =~ /^\@/ ) {
                opendir my $testDir, "debian/tests/test_modules/$mod";
                my @subMods = grep { /^[^\.]/ } ( readdir $testDir );
                $self->dolink( "debian/tests/test_modules/$mod/$_",
                    "node_modules/$mod/$_" )
                  foreach (@subMods);
                push @testlinks, map { "$mod/$_" } @subMods;
            }
            else {
                $self->dolink( "debian/tests/test_modules/$mod",
                    "node_modules/$mod" );
                push @testlinks, $mod;
            }
        }
    }
    eval {
        if ( my $p = $self->main_package ) {
            $self->dolink( $p, 'node_modules/' . $self->pjson($p)->{name} );
            push @testlinks, $self->pjson($p)->{name};
        }
    };
    foreach (@dirs) {
        if ( -e "debian/nodejs/$_/test" ) {
            my @count = (m#(/)#g);
            print_and_doit( { chdir => $self->get_buildpath . "/$_" },
                'sh', '-ex',
                ( '../' x ( scalar(@count) + 1 ) ) . "debian/nodejs/$_/test" );
        }
    }
    my $testfile = (
        -e 'debian/nodejs/test'
        ? 'debian/nodejs/test'
        : 'debian/tests/pkg-js/test'
    );
    if ( -e $testfile ) {
        $self->doit_in_builddir( '/bin/sh', '-ex', $testfile );
    }
    else {
        $self->doit_in_builddir( '/usr/bin/node', '-e',
            'require("./' . $self->main_package . '")' )
          if $self->main_package;
    }
    foreach (@testlinks) {
        print "Removing node_modules/$_\n";
        unlink "node_modules/$_";
    }
}

# install step:
#  - if no debian/install file found, install automatically module
#  - install components

sub install {
    my $self    = shift;
    my $destdir = shift;
    my @pkgs    = getpackages();
    my ($package) =
      $#pkgs ? ( grep { $_ =~ /^node-/ } getpackages() ) : @pkgs;
    ($package) = @pkgs unless ($package);
    my $realArchPath = (
        package_is_arch_all($package)
        ? '/usr/share/nodejs'
        : '/usr/lib/'
          . dpkg_architecture_value("DEB_HOST_MULTIARCH")
          . '/nodejs'
    );
    my $archPath = "$destdir/$realArchPath";
    my $node_module =
        $self->main_package
      ? $self->pjson( $self->main_package )->{name}
      : '';

    unless ($node_module) {
        warning <<EOT;
##############################################################
# /!\ No name found for main module, this may break install  #
# To fix this, fix main component path in debian/nodejs/main #
##############################################################
EOT
    }
    my $components = $self->component_list;
    my @dirs       = ( sort keys %$components );

    # main install
    my $path = "$archPath/$node_module";
    $self->install_module( $destdir, $archPath, $path, $self->main_package,
        @dirs )
      if $node_module;

    # Component install
    #return unless (@dirs);

    # Search root components
    my %root_cmp;
    if ( -e 'debian/nodejs/root_modules' ) {
        my @lines = $self->open_file('debian/nodejs/root_modules');
        if ( grep { $_ eq '*' } @lines ) {
            %root_cmp = map { ( $_ => 1 ) } @dirs;
        }
        else {
            %root_cmp = map {
                my $s = $_;
                map { ( $_ => 1 ) } glob($s)
            } $self->open_file('debian/nodejs/root_modules');
        }
    }
    elsif ( !$self->main_package ) {
        %root_cmp = map {
            my $s = $_;
            map { ( $_ => 1 ) } glob($s)
        } @dirs;
    }

    $path = "$path/node_modules";
    if ( -e 'debian/nodejs/submodules' ) {
        my @d = map { chomp; /^\w/ ? ( glob($_) ) : () }
          $self->open_file('debian/nodejs/submodules');
        unless (@d) {
            print
"Components auto-install is skipped by empty debian/nodejs/submodules";
            return;
        }
        my @tmp;
      CMP: foreach my $cmp (@d) {
            next CMP
              if $self->main_package and $cmp eq $self->main_package;
            foreach my $dir (@dirs) {
                if ( $dir eq $cmp or $components->{$dir} eq $cmp ) {
                    push @tmp, $dir;
                    next CMP;
                }
            }
            warning
              qq{# /!\ In debian/nodejs/submodules, "$cmp" matches nothing\n};
        }
        @dirs = @tmp;
    }

    my @provides;
    foreach my $dir (@dirs) {
        $self->install_module(
            $destdir,
            $archPath,
            (
                $root_cmp{$dir}
                ? "$destdir/$realArchPath"
                : $path
              )
              . "/$components->{$dir}",
            $dir
        );
        if ( $root_cmp{$dir} ) {
            my $tmp = 'node-' . $self->pjson($dir)->{name};
            $tmp =~ s/[\@_\/]/-/g;
            $tmp =~ s/--*/-/g;
            my $v;
            if ( defined( $v = $self->pjson($dir)->{version} ) ) {
                $tmp .= " (= $v)";
            }
            push @provides, $tmp;
        }
    }
    my $pname = "node-$node_module";
    $pname =~ s/[\@_\/]/-/g;
    $pname =~ s/--*/-/g;
    if ( $package ne $pname ) {
        my $tmp = $pname;
        my $v;
        if ( -e $self->main_package . "/package.json"
            and defined( $v = $self->pjson( $self->main_package )->{version} ) )
        {
            $tmp .= " (= $v)";
        }
        else {
            print STDERR "# No version found in main package!";
        }
        unshift @provides, $tmp;
    }
    if (@provides) {
        print "Populate \${nodejs:Provides}:\n";
        print " + $_\n" foreach (@provides);
        addsubstvar( $package, 'nodejs:Provides', join( ', ', @provides ) );
    }

    {
        my ( $in, $src );
        if ( -e 'debian/node-node-expat.substvars' ) {
            run [
                'grep', '-Po',
                'shlibs:Depends=.*libnode\d+ \(>= \K\S+(?=\))',
                'debian/node-node-expat.substvars'
              ],
              \$in, \$src;
        }
        if ( not $src ) {
            run [
                'dpkg-query', '--showformat=${source:Upstream-Version}',
                '--show',     'nodejs'
              ],
              \$in, \$src;
        }
        if ( not $src or $? ) {
            print STDERR "### node version not found\n";
        }
        else {
            chomp $src;
            addsubstvar( $package, 'nodejs:Version', $src );
            print "Set \${nodejs:Version} to $src\n";
        }
    }

    # Links
    if ( -e "debian/nodejs/links" ) {
        my $tmp = tmpdir($package);
        map {
            my $line = $_;
            my ( $src, $dest ) =
              map { m#^/# ? $_ : "$realArchPath/$_" } split /\s+/, $line;
            unless ( $src and $dest ) {
                error "Bad line in debian/nodejs/links: $_";
            }
            print "Linking $dest to $src\n";
            make_symlink( $dest, $src, $tmp );
        } $self->open_file("debian/nodejs/links");
    }

}

# clean step: try to clean node_modules directories
sub clean {
    my $self       = shift;
    my $components = $self->component_list;
    my @dirs       = ( sort keys %$components );
    foreach my $dir ( $self->main_package, @dirs ) {
        $self->doit_in_sourcedir( 'rm', '-rf', "$dir/node_modules/.cache" );
        if ( -e "debian/nodejs/$dir/extlinks" ) {
            map { $self->do_unlink("$dir/node_modules/$_"); }
              $self->open_file("debian/nodejs/$dir/extlinks");
        }
        if ( -e "debian/nodejs/$dir/extcopies" ) {
            map {
                $self->doit_in_builddir( 'rm', '-rf', "$dir/node_modules/$_" );
            } $self->open_file("debian/nodejs/$dir/extcopies");
        }
    }
    if ( -e 'debian/nodejs/component_links' ) {
        map {
            error "Malformed line $. in debian/nodejs/component_links: $_"
              unless /^([\w\-\.\/]+)\s+([\w\-\.\/]+)$/;
            my ( $src, $dest ) = ( $1, $2 );
            $self->do_unlink("$dest/node_modules/$components->{$src}");
        } $self->open_file('debian/nodejs/component_links');
    }
    foreach my $component ( $self->main_package, @dirs ) {
        my $package = $components->{$component};

        # Error not catched here
        $self->do_unlink("node_modules/$package") if $package;
    }
    if ( -d 'debian/build_modules' ) {
        opendir my $buildDir, 'debian/build_modules';
        my @buildMods = grep { /^[^\.]/ } ( readdir $buildDir );
        foreach my $mod (@buildMods) {
            if ( $mod =~ /^\@/ ) {
                opendir my $buildDir, "debian/build_modules/$mod";
                my @subMods = grep { /^[^\.]/ } ( readdir $buildDir );
                $self->do_unlink("node_modules/$mod/$_") foreach (@subMods);
                eval { rmdir "node_modules/$mod" };
            }
            else {
                $self->do_unlink("node_modules/$mod");
            }
        }
    }
}

1;
