#!/usr/bin/env perl

# ---------------------------------------------------------------------------
# Copyright (C) 2008-2020 TJ Saunders <tj@castaglia.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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
# ---------------------------------------------------------------------------

use strict;

use File::Basename qw(basename);
use Getopt::Long;

Getopt::Long::Configure("no_ignorecase");

my $prog = basename($0);

my $compiler = q(/usr/bin/clang);
my $cflags = q(-DDARWIN21 -DDARWIN21_6_0 -g2 -pipe -Os -isysroot/Library/Developer/CommandLineTools/SDKs/MacOSX12.sdk -arch arm64 -Wall -fno-omit-frame-pointer -fno-strict-aliasing -Werror=implicit-function-declaration -DPR_SHARED_MODULE);
my $cppflags = q(-I/opt/local/include -isysroot/Library/Developer/CommandLineTools/SDKs/MacOSX12.sdk);
my $ltdl_ldflags = q(-avoid-version -export-dynamic -module);
my $sbindir = q(/opt/local/sbin);
my $includedir = q(/opt/local/include);
my $installer = q(/usr/bin/install -c);
my $install_strip = q();
my $libexecdir = q(/opt/local/libexec);
my $libtool = 'libtool';

if (!defined($ENV{LIBTOOL})) {
  if ($^O eq 'darwin') {
    $libtool = 'glibtool';
  }

} else {
  $libtool = $ENV{LIBTOOL};
}

my $opts = {};
GetOptions($opts, 'c|compile', 'i|install', 'd|clean', 'h|help', 'name=s',
  'D=s@', 'I=s@', 'L=s@', 'l=s@', 'W=s@');

if ($opts->{h}) {
  usage();
  exit 0;
}

# Make sure we can query proftpd to find out its list of installed modules.
# Unless we see mod_dso listed, there's no point in compiling a shared
# module for proftpd to use.

my $proftpd = "$sbindir/proftpd";
unless (-x $proftpd) {
  print STDERR "$proftpd not found or not executable\n";
  exit 1;
}

unless (grep /mod_dso/, `$proftpd -l`) {
  print STDERR "\nYour installed proftpd does not support shared modules/DSOs.\n";
  print STDERR "Make sure the --enable-dso configure option is used when\n";
  print STDERR "compiling proftpd.\n\n";
  exit 1;
}

# If the libtool-specific linker flags are empty, it means that the
# script was generated by a configure script lacking the --enable-dso option.
unless (length($ltdl_ldflags) > 0) {
  print STDERR "\nMissing the required libtool flags.\n";
  print STDERR "Make sure the --enable-dso configure option is used when\n";
  print STDERR "compiling proftpd.\n\n";
  exit 1;
}

# Now, depending on the requested mode (compile/install/clean), build up
# and execute the commands.

my $mod_name = get_module_name();

if (defined($opts->{c})) {
  my $srcs = [];
  my $objs = [];

  my $configure_script;

  foreach my $file (@ARGV) {
    if ($file =~ /\.c$/) {
      push(@$srcs, $file);

      if ($file =~ /\/?mod_[^\/]+\.c$/) {
        $configure_script = $file;
        $configure_script =~ s/(\/?)mod_[^\/]+\.c$/\1configure/;

        if ($configure_script !~ /^\//) {
          $configure_script = './' . $configure_script;
        }
      }

      my $obj = $file;
      $obj =~ s/\.c$/\.lo/;

      # Handle an argument such as 'contrib/mod_sql_sqlite.c' by keeping
      # just the name portion of the path.
      $obj =~ s/^(.*)\///g;

      push(@$objs, $obj);

    } else {
      print STDERR "Cannot compile non-.c file $file, aborting\n";
      exit 1;
    }
  }

  if (defined($configure_script)) {
    # Check for any configure script for this module.  If present, error out
    # for now.
    if (-f $configure_script) {
      print STDERR "\nCannot compile $mod_name using prxs.  Use existing $configure_script script instead:\n\n";

      if ($configure_script ne './configure') {
        my $mod_dir = $configure_script;
        $mod_dir =~ s/configure$//g;
        print STDERR "  cd $mod_dir\n";
      }

      print STDERR "  ./configure\n";
      print STDERR "  make\n";
      print STDERR "  make install\n";
      exit 1;
    }
  }

  foreach my $def (@{ $opts->{D} }) {
    if ($def =~ /^(\S+)=(\S+)$/) {
      $cflags .= " -D'$1=$2'";

    } else {
      $cflags .= " -D$def";
    }
  }

  $cflags .= " -I. -I$includedir/proftpd";

  foreach my $incdir (@{ $opts->{I} }) {
    $cflags .= " -I$incdir";
  }

  my $cmds = [];
  foreach my $src (@$srcs) {
    push(@$cmds, "$libtool --mode=compile $compiler $cflags -c $src");
  }

  run_cmds($cmds);

  my $objlist = '';
  foreach my $obj (@$objs) {
    $objlist .= " $obj";
  }

  my $ldflags .= " $ltdl_ldflags";

  foreach my $libdir (@{ $opts->{L} }) {
    $ldflags .= " -L$libdir";
  }

  # Scan through the .c files, looking for the $Libraries$ hint that
  # proftpd's build system uses.
  foreach my $src (@$srcs) {
    if (open(my $fh, "< $src")) {
      while (my $line = <$fh>) {
        chomp($line);

        if ($line =~ /\$Libraries:\s+(.*)?\$/) {
          my $hint = $1;

          # Assume that the library hint list is space-separated; add them
          # to the $opts hashref.  Don't forget to strip of the '-l' prefix;
          # that is added back later in the handling of $opts.
          my $libs = [split(/\s+/, $hint)];
          foreach my $lib (@$libs) {
            $lib =~ s/^\-l//;
            push(@{ $opts->{l} }, $lib);
          }

          last;
        }
      }

      close($fh);

    } else {
      print STDERR "Unable to scan $src for \$Libraries\$ hint: $!\n";
    }
  }

  my $libs = "";
  foreach my $lib (@{ $opts->{l} }) {
    $libs .= " -l$lib";
  }

  $cmds = [];
  push(@$cmds, "$libtool --mode=link $compiler -o $mod_name.la -rpath $libexecdir $ldflags $objlist $libs");

  run_cmds($cmds);
}

if (defined($opts->{i})) {
  my $cmds = [];
  push(@$cmds, "$libtool --mode=install $installer $install_strip $mod_name.la $ENV{DESTDIR}$libexecdir");

  run_cmds($cmds);

  # Don't forget to remind the user to manually edit their proftpd.conf
  # and add the LoadModule to load the just-installed module.

  my $load_mod_name = $mod_name;
  $load_mod_name =~ s/^(.*)\///;

  print STDOUT "\nTo load your newly installed module into proftpd, be sure\n";
  print STDOUT "to edit your proftpd.conf and add the following:\n\n";
  print STDOUT "  <IfModule mod_dso.c>\n";
  print STDOUT "    LoadModule $load_mod_name.c\n";
  print STDOUT "  </IfModule>\n\n";
  print STDOUT "and then restart your proftpd server, so that the config change\n";
  print STDOUT "becomes live.\n\n";
}

if (defined($opts->{d})) {
  my $cmds = [];
  push(@$cmds, "$libtool --mode=clean rm -f $mod_name.la *.lo");

  run_cmds($cmds);
}

if (!defined($opts->{c}) &&
    !defined($opts->{i}) &&
    !defined($opts->{d})) {
  print STDERR "No compile, install, or clean mode requested, exiting\n";
  exit 1;
}

exit 0;

sub get_module_name {
  # Determine the name of the module (e.g. "mod_foo") being operated upon.
  if (defined($opts->{n})) {
    return $opts->{n};
  }

  foreach my $file (@ARGV) {
    # Handle an argument such as 'contrib/mod_sql_sqlite.c' by keeping
    # just the name portion of the path.

    if ($file =~ /\/?mod_([^\/]+)\.(c|la)$/) {
      return "mod_$1";
    }
  }

  return "mod_unknown";
}

sub run_cmds {
  my $cmds = shift;

  foreach my $cmd (@$cmds) {
    print STDOUT "$cmd\n";

    my $res = system($cmd);
    if ($res) {
      print STDERR "$prog: error executing command (", $res >> 8, ")\n";
      exit 1;
    }
  }
}

sub usage {
  my $prog = basename($0);

  print STDOUT <<EOU;

usage: $prog <action> <opts> <source files>

Actions:

 -c, --compile          Compiles the listed .c source files into a proftpd
                        DSO module.

 -i, --install          Installs a compiled proftpd DSO module into the
                        directory where proftpd expects to find loadable
                        DSO modules.

 -d, --clean            Removes any generated files, returning the build
                        directory to a clean state.

Options:

 -h, --help             Displays this message.

 -n, --name             Tells prxs the name of the module being compiled.
                        By default, prxs determines the module name from
                        the list of .c files listed, expecting to see a
                        "mod_\$name.c" file.

 -D key                 Passes these macros through to the compilation step.
 -D key=value           Note that the space before the key is important.

 -I includedir          Specify additional include file search directories.
                        Note that the space before the directory is important.

 -L libdir              Specify additional library file search directories.
                        Note that the space before the directory is important.

 -l library             Specify additional libraries for linking.
                        Note that the space before the library name is important.

At least one of the above actions must be specified when using prxs.  More
than one action can be specified at the same time.

To use prxs all in one step, you could do:

  prxs -c -i -d mod_custom.c

EOU
}
