#!/usr/bin/perl
# $OpenBSD: libtool,v 1.18 2008/02/17 02:01:42 jakemsr Exp $

# Copyright (c) 2007 Steven Mestdagh <steven@openbsd.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

use strict;
use warnings;
use Cwd qw(getcwd abs_path);
use File::Basename;
use File::Glob ':glob';
use File::Path;
use Getopt::Long;
use Getopt::Std;

use subs qw(
	help
	handle_special_chars
	notyet
	perform
	parse_version_info
	guess_implicit_mode
	create_symlinks
	write_lo_file
	write_la_file
	write_prog_wrapper
	is_prog_wrapper
	parse_file
	resolve_la
	parse_linkargs
	find_la
	find_lib
	process_deplibs
	reverse_zap_duplicates
	linkcmd
	generate_objlist
	get_search_dirs
	);

use constant {
	OBJECT	=> 0,
	LIBRARY	=> 1,
	PROGRAM	=> 2,
};

my $version = '1.5.22'; # pretend to be this version of libtool
my @no_shared_archs = qw(m88k vax sh);
# XXX my $machine_arch = `machine -a`;
my $machine_arch = 'amd64';
(my $gnu_arch = $machine_arch) =~ s/amd64/x86_64/;
my @valid_modes = qw(clean compile execute finish install link uninstall);
my @valid_src = qw(asm c cc cpp cxx f s);
my $cwd = getcwd();
my $ltdir = '.libs';
my $picflags = '-fPIC -DPIC';
my $sharedflag = '-shared';
my $instlibdir = '/usr/local/lib';
my @libsearchdirs;
$instlibdir = $ENV{'LIBDIR'} if defined $ENV{'LIBDIR'};

my $mode;
my $D = 0;		# debug flag
my $verbose = 1;
my $dry = 0;		# dry-run

my %opts;		# options passed to libtool
my @Ropts;		# -R options
my @tags;		# list of --tag options passed to libtool
my @deplibs;		# list of dependent libraries (both -L and -l flags)
my %libs;		# libraries
my %libstofind;
my @orderedlibs;	# ordered library keys (may contain duplicates)
my %dirs;		# paths to find libraries
my %file_parsed;	# which files have been parsed
my $res_level = 0;	# resolve level
my $parse_level = 0;	# parse recursion level
my $performed = 0;	# number of commands executed via system()

# build static/shared objects?
my $static = 1;
my $shared = 0;
my $noshared = 0;
if (grep { $_ eq $machine_arch } @no_shared_archs) {
	$noshared = 1;
}

# put a priority in the dir hash
# always look here
$dirs{'/usr/lib'} = 3;

my $gp = new Getopt::Long::Parser;
# require_order so we stop parsing at the first non-option or argument,
# instead of parsing the whole ARGV.
$gp->configure(	'no_ignore_case',
		'pass_through',
		'no_auto_abbrev',
		'require_order'
	);
$gp->getoptions('config' => \&config,
		'debug' => \$D,
		'dry-run' => sub { $dry = 1; },
		'features' => \&features,
		'finish' => sub { $mode = 'finish'; },
		'help' => \&help, # does not return
		'mode=s{1}' => \$mode,
		'quiet' => sub { $verbose = 0; },
		'silent' => sub { $verbose = 0; },
		'tag=s{1}' => \@tags,
		'version' => sub { print "$version\n" ; exit(0); },
	);

# what are we going to run (cc, c++, ...)
my $ltprog = shift @ARGV or die "no libtool command\n";

# check mode and guess it if needed
if (!($mode && grep { $_ eq $mode } @valid_modes)) {
	$mode = guess_implicit_mode();
	if ($mode) {
		print "implicit mode: $mode\n" if $D;
	} else {
		die "MODE must be one of:\n@valid_modes\n";
	}
}

# from here, options may be intermixed with arguments
$gp->configure('permute');

if ($mode eq 'compile') {
	# deal with multi-arg ltprog
	while (@ARGV && $ltprog !~ m/(cc|c\+\+|f77|g77|asm)$/) {
		my $arg = shift @ARGV;
		$ltprog .= " $arg";
	}
	$gp->getoptions('o=s'		=> \$opts{'o'},
			'prefer-pic'	=> \$opts{'prefer-pic'},
			'prefer-non-pic'=> \$opts{'prefer-non-pic'},
			'static'	=> \$opts{'static'},
			);
	# XXX options ignored: -prefer-pic and -prefer-non-pic
	my $cmd;
	my $pic = 0;
	my $nonpic = 1;
	# assume we need to build pic objects
	$pic = 1 if (!$noshared);
	$nonpic = 0 if ($pic && grep { $_ eq 'disable-static' } @tags);
	$pic = 0 if ($nonpic && grep { $_ eq 'disable-shared' } @tags);
	$nonpic = 1 if ($opts{'static'});

	my $outfile = $opts{'o'};
	my $srcfile;
	# XXX check whether -c flag is present and if not, die?
	if (!$outfile) {
		# XXX sometimes no -o flag is present and we need another way
		my $srcre = join '|', @valid_src;
		my $found = 0;
		foreach my $a (@ARGV) {
			if ($a =~ m/\.($srcre)$/i) {
				$srcfile = $a;
				$found = 1;
				last;
			}
		}
		$found or die "cannot find source file in command\n";
		# the output file ends up in the current directory
		$outfile = basename $srcfile;
		$outfile =~ s/\.($srcre)$//i;
		$outfile .= '.lo';
	}
	print "srcfile = $srcfile\n" if ($srcfile && $D);
	# put output file in . if no -o flag is given
	my $odir = '.';
	# fix extension if needed
	$outfile =~ s/\.o$/.lo/;
	$odir = dirname $outfile if ($opts{'o'});
	my $ofile = basename $outfile;
	print "outfile = $odir/$ofile\n" if $D;
	(my $nonpicobj = $ofile) =~ s/\.lo$/.o/;
	my $picobj = "$ltdir/$nonpicobj";

	handle_special_chars(\@ARGV);

	mkdir "$odir/$ltdir" if (! -d "$odir/$ltdir");
	if ($pic) {
		$cmd = $ltprog;
		$cmd .= " @ARGV" if (@ARGV);
		$cmd .= " $picflags";
		$cmd .= " -o ";
		$cmd .= ($odir eq '.') ? '' : $odir.'/';
		$cmd .= $picobj;
		perform($cmd);
	}
	if ($nonpic) {
		$cmd = $ltprog;
		$cmd .= " @ARGV" if (@ARGV);
		$cmd .= " -o ";
		$cmd .= ($odir eq '.') ? '' : $odir.'/';
		$cmd .= $nonpicobj;
		perform($cmd);
	}
	write_lo_file($outfile, ($pic) ? $picobj : '',
			($nonpic) ? $nonpicobj : '');
} elsif ($mode eq 'install') {
	# deal with multi-arg ltprog (e.g. /bin/sh install-sh ...)
	while (@ARGV && $ltprog !~ m/(install([.-]sh)?|cp)$/) {
		my $arg = shift @ARGV;
		$ltprog .= " $arg";
	}
	# we just parse the options in order to find the actual arguments
	my @argvcopy = @ARGV;
	my %install_opts;
	if ($ltprog =~ m/install([.-]sh)?$/) {
		getopts('BbCcdf:g:m:o:pSs', \%install_opts);
		if (@ARGV < 2 && (!defined $install_opts{'d'} && @ARGV == 1)) {
			die "wrong number of arguments for install\n";
		}
	} elsif ($ltprog =~ m/cp$/) {
		getopts('HLPRfipr', \%install_opts);
		if (@ARGV < 2) {
			die "wrong number of arguments for install\n";
		}
	} else {
		die "unsupported install program $ltprog\n";
	}
	my @instopts = @argvcopy[0 .. (@argvcopy - @ARGV - 1)];
	my $dst = pop @ARGV;
	# assume dst is a directory
	my $dstdir = $dst;
	my @src = @ARGV;
	# dst is not a directory, i.e. a file
	if (! -d $dst) {
		if (@src > 1) {
			# XXX not really libtool's task to check this
			die "multiple source files combined with file destination";
		} else {
			$dstdir = dirname $dst;
		}
	}
	my %toinstall;
	my %tosymlink;	# for libraries with a -release in their name
	foreach my $s (@src) {
		my $dstfile = basename $s;
		# resolve symbolic links, so we don't try to test later
		# whether the symlink is a program wrapper etc.
		if (-l $s) {
			$s = readlink($s) or die "Cannot readlink $s";
		}
		my $srcdir = dirname $s;
		my $srcfile = basename $s;
		print "srcdir = $srcdir\nsrcfile = $srcfile\n" if $D;
		print "dstdir = $dstdir\ndstfile = $dstfile\n" if $D;
		if ($srcfile =~ m/^\S+\.la$/) {
			# we are installing a .la library
			if ($ltprog =~ m/install([.-]sh)?$/) {
				push @instopts, '-m 644';
			}
			my %lainfo;
			parse_file($s, \%lainfo);
			# replace info where needed when installing the .la file
			my $sharedlib = $lainfo{'dlname'};
			my $staticlib = $lainfo{'old_library'};
			my @libnames = split /\s+/, $lainfo{'library_names'};
			my $laipath = "$srcdir/$ltdir/$srcfile".'i';
			$toinstall{"$srcdir/$ltdir/$staticlib"} = "$dstdir/$staticlib"
				if ($staticlib);
			$toinstall{"$srcdir/$ltdir/$sharedlib"} = "$dstdir/$sharedlib"
				if ($sharedlib);
			$toinstall{"$laipath"} = "$dstdir/$dstfile"
				if ($sharedlib);
			foreach my $n (@libnames) {
				$tosymlink{$n} = $sharedlib if ($n ne $sharedlib);
			}
		} elsif (-f "$srcdir/$ltdir/$srcfile" && is_prog_wrapper($s)) {
			$toinstall{"$srcdir/$ltdir/$srcfile"} = $dst;
		} else {
			$toinstall{$s} = $dst;
		}
	}
	while (my ($s, $d) = each %toinstall) {
		my @realinstopts = @instopts;
		# do not try to strip .la files
		if ($s =~ m/\.la$/) {
			map { $_ = '' if $_ eq '-s' } @realinstopts;
		}
		perform("$ltprog @realinstopts $s $d");
	}
	while (my ($d, $s) = each %tosymlink) {
		perform("cd $dstdir && rm -f $d && ln -s $s $d");
	}
	if (defined $install_opts{'d'}) {
		perform("$ltprog @instopts @ARGV");
	}
} elsif ($mode eq 'link') {
	my $cmd;
	$gp->getoptions('all-static'		=> \$opts{'all-static'},
			'avoid-version'		=> \$opts{'avoid-version'},
			'dlopen=s{1}'		=> \$opts{'dlopen'},
			'dlpreopen=s{1}'	=> \$opts{'dlpreopen'},
			'export-dynamic'	=> \$opts{'export-dynamic'},
			'export-symbols=s'	=> \$opts{'export-symbols'},
			'export-symbols-regex=s'=> \$opts{'export-symbols-regex'},
			'module'		=> \$opts{'module'},
			'no-fast-install'	=> \$opts{'no-fast-install'},
			'no-install'		=> \$opts{'no-install'},
			'no-undefined'		=> \$opts{'no-undefined'},
			'o=s'			=> \$opts{'o'},
			'objectlist=s'		=> \$opts{'objectlist'},
			'precious-files-regex=s'=> \$opts{'precious-files-regex'},
			'prefer-pic'		=> \$opts{'prefer-pic'},
			'prefer-non-pic'	=> \$opts{'prefer-non-pic'},
			'release=s'		=> \$opts{'release'},
			'rpath=s'		=> \$opts{'rpath'},
			'R=s'			=> \@Ropts,
			'shrext=s'		=> \$opts{'shrext'},
			'static'		=> \$opts{'static'},
			'thread-safe'		=> \$opts{'thread-safe'},
			'version-info=s{1}'	=> \$opts{'version-info'},
			'version_info=s{1}'	=> \$opts{'version-info'},
			'version-number=s{1}'	=> \$opts{'version-info'},
		);
	# XXX options ignored: dlopen, dlpreopen, export-dynamic,
	# 	export-symbols, export-symbols-regex, no-fast-install,
	# 	no-install, no-undefined, objectlist, precious-files-regex,
	# 	shrext, thread-safe, prefer-pic, prefer-non-pic

	@libsearchdirs = get_search_dirs();

	my $outfile = $opts{'o'};
	if (!$outfile) {
		die "no output file given.\n";
	}
	print "outfile = $outfile\n" if $D;
	my $odir = dirname $outfile;
	my $absodir = abs_path($odir);
	my $ofile = basename $outfile;

	# what are we linking?
	my $linkmode = PROGRAM;
	if ($ofile =~ m/\.la$/) {
		$linkmode = LIBRARY;
	} elsif ($ofile =~ s/\.a$/.la/) {
		$outfile =~ s/\.a$/.la/;
		$linkmode = LIBRARY;
		$noshared = 1; $static = 1; # XXX improve!
	}
	print "linkmode: $linkmode\n" if $D;

	handle_special_chars(\@ARGV);

	my $argvstring = join ' ', @ARGV;
	$argvstring = parse_linkargs($argvstring, 1);
	@ARGV = split /\s+/, $argvstring;
	print "deplibs = @deplibs\n" if $D;

	# eat multiple version-info arguments, we only accept the first.
	map { $_ = '' if ($_ =~ m/\d+:\d+:\d+/); } @ARGV;

	my @objs;
	my @sobjs;
	my $allpicobj = generate_objlist(\@objs, \@sobjs);
	print "objs = @objs\n" if $D;
	print "sobjs = @sobjs\n" if $D;

	if ($linkmode == PROGRAM) {
		# XXX give higher priority to dirs of not installed libs
		# XXX no static linking yet here
		my @tmpcmd = linkcmds($ofile, $ofile, $odir, PROGRAM, 1, \@objs);
		$cmd = $tmpcmd[0];
		perform($cmd);
		write_prog_wrapper($outfile);
		chmod 0755, $outfile;
	} elsif ($linkmode == LIBRARY) {
		(my $libname = $ofile) =~ s/\.la$//;	# remove extension
		my $staticlib = $libname.'.a';
		my $sharedlib = $libname.'.so';
		my $sharedlib_symlink;

		# XXX how is the creation of a shared library switched on?
		# XXX to do: deal with -rpath correctly
		$shared = 1 if ($opts{'version-info'} ||
				$opts{'avoid-version'} ||
				$opts{'module'} ||
				$opts{'rpath'} );
		if ($opts{'static'}) {
			$shared = 0;
			$static = 1;
		}
		$shared = 0 if $noshared;
		my $sover = '0.0';
		# environment overrides -version-info
		(my $envlibname = $libname) =~ s/[.+-]/_/g;
		my ($current, $revision, $age) = (0, 0, 0);
		if ($ENV{"${envlibname}_ltversion"}) {
			$sover = $ENV{"${envlibname}_ltversion"};
			($current, $revision) = split /\./, $sover;
			$age = 0;
		} elsif ($opts{'version-info'}) {
			($current, $revision, $age) = parse_version_info($opts{'version-info'});
			$sover = "$current.$revision";
		}
		if ($opts{'release'}) {
			$sharedlib_symlink = $sharedlib;
 			$sharedlib = $libname.'-'.$opts{'release'}.'.so';
		}
		if (!$opts{'avoid-version'}) {
			$sharedlib .= ".$sover";
			if ($opts{'release'}) {
				$sharedlib_symlink .= ".$sover";
			}
		}

		# XXX add error condition somewhere...
		$static = 0 if ($shared && grep { $_ eq 'disable-static' } @tags);
		$shared = 0 if ($static && grep { $_ eq 'disable-shared' } @tags);

		print "SHARED: $shared\nSTATIC: $static\n" if $D;

		my %lainfo;
		if ($shared) {
			$lainfo{'dlname'} = $sharedlib;
			$lainfo{'library_names'} = $sharedlib;
			$lainfo{'library_names'} .= " $sharedlib_symlink"
				if ($opts{'release'});
			perform(linkcmds($ofile, $sharedlib, $odir, LIBRARY, 1, \@sobjs));
			print "sharedlib: $sharedlib\n" if $D;
			$lainfo{'current'} = $current;
			$lainfo{'revision'} = $revision;
			$lainfo{'age'} = $age;
		}
		if ($static) {
			$lainfo{'old_library'} = $staticlib;
			perform(linkcmds($ofile, $staticlib, $odir, LIBRARY, 0, ($allpicobj) ? \@sobjs : \@objs));
			print "staticlib: $staticlib\n" if $D;
		}
		$lainfo{'installed'} = 'no';
		$lainfo{'shouldnotlink'} = $opts{'module'} ? 'yes' : 'no';
		my @Rflags = @Ropts;
		map { $_ = "-R$_" } @Rflags;
		my $deplibstring = join ' ', @deplibs;
		$deplibstring = "@Rflags $deplibstring" if (@Rflags);
		@deplibs = split /\s+/, $deplibstring;
		my @finaldeplibs = reverse_zap_duplicates(@deplibs);
		$deplibstring = join ' ', @finaldeplibs;
		$lainfo{'dependency_libs'} = $deplibstring;
		if ($opts{'rpath'}) {
			$lainfo{'libdir'} = $opts{'rpath'};
		} else {
			# XXX sensible default?
			$lainfo{'libdir'} = $instlibdir;
		}
		write_la_file($outfile, $ofile, \%lainfo);
		perform("cd $odir/$ltdir && rm -f $ofile && ln -s ../$ofile $ofile");
		if ($shared) {
			my $lai = "$odir/$ltdir/$ofile".'i';
			$lainfo{'dependency_libs'} = process_deplibs($deplibstring);
			$lainfo{'installed'} = 'yes';
			# write .lai file (.la file that will be installed)
			write_la_file($lai, $ofile, \%lainfo);
		}
	}
} elsif ($mode eq 'finish' || $mode eq 'clean' || $mode eq 'uninstall') {
	# don't do anything
	exit 0;
} elsif ($mode eq 'execute') {
	# XXX check whether this is right
	perform("$ltprog @ARGV");
} else {
	die "MODE=$mode not implemented yet.\n";
}

if ($performed == 0) {
	die "no commands to execute.\n"
}

###########################################################################

sub help
{
	print <<EOF
Usage: $0 [options]
--config - print configuration
--debug	- turn on debugging output
--dry-run - don't do anything, only show what would be done
--features - show basic configuration information
--finish - same as `--mode=finish'
--help - this message
--mode=MODE - use operation mode MODE
--quiet - do not print informational messages
--silent - same as `--quiet'
--tag - 
--version - print version of libtool
EOF
;
	exit 1;
}

sub notyet
{
	die "option not implemented yet.\n";
}

# display and execute a list of commands via system(), and die on error.
sub perform
{
	my @cmd = @_;

	foreach my $c (@cmd) {
		print "$c\n" if ($verbose || $dry || $D);
		if (!$dry) {
			system($c) == 0 or die "Error while executing:\n$c\n";
		}
	}
	$performed++;
}

# XXX incomplete
sub config
{
	print "objdir=$ltdir\n";
	print "...\n";
	exit 0;
}

sub features
{
	chomp (my $osver = `uname -r`);	# XXX avoid calling uname
	print "host: $gnu_arch-unknown-openbsd$osver\n";
	print (($noshared) ? "dis" : "en", "able shared libraries\n");
	print ((!$static)  ? "dis" : "en", "able static libraries\n");
	exit 0;
}

# convert 4:5:8 into a list of numbers
sub parse_version_info
{
	my $vinfo = shift;

	if ($vinfo =~ m/(\d+):(\d+):(\d+)/) {
		return ($1, $2, $3);
	} else {
		die "error parsing -version-info $vinfo\n";
	}
}

sub create_symlinks
{
	my $dir = shift;
	my $libfiles = shift;

	if (! -d $dir) {
		mkdir $dir or die "cannot create directory: $!\n";
	}
	foreach my $f (@$libfiles) {
		next if ($f =~ m/\.a$/);
		my $libfile = basename $f;
		print "ln -s $f $dir/$libfile\n" if $D;
		if (! -f "$dir/$libfile") {
			symlink abs_path($f), "$dir/$libfile" or die "cannot create symlink: $!\n";
		}
	}
}

# write a libtool object file
sub write_lo_file
{
	my $filename = shift;
	my $picobj = shift;
	my $nonpicobj = shift;

	my $name = basename $filename;

	open(my $lo, '>', $filename) or die "cannot write $filename: $!\n";
	print "creating $filename\n" if ($verbose || $D);
	print $lo <<EOF
# $name - a libtool object file
# Generated by libtool $version
#
pic_object='$picobj'
non_pic_object='$nonpicobj'
EOF
;
}

# XXX not sure how much of this cruft we need
sub write_la_file
{
	my $filename = shift;
	my $name = shift;
	my $lainfo = shift;

	my $sharedlibname = $lainfo->{'dlname'} || '';
	my $staticlibname = $lainfo->{'old_library'} || '';
	my $librarynames = $lainfo->{'library_names'} || '';
	my $deplibs = $lainfo->{'dependency_libs'};
	my ($current, $revision, $age) = ('', '', '');
	$current = $lainfo->{'current'} if (defined $lainfo->{'current'});
	$revision = $lainfo->{'revision'} if (defined $lainfo->{'revision'});
	$age = $lainfo->{'age'} if (defined $lainfo->{'age'});
	my $installed = $lainfo->{'installed'};
	my $shouldnotlink = $lainfo->{'shouldnotlink'};
	my $libdir = $lainfo->{'libdir'};

	open(my $la, '>', $filename) or die "cannot write $filename: $!\n";
	print "creating $filename\n" if ($verbose || $D);
	print $la <<EOF
# $name - libtool library file
# Generated by libtool $version
#
# Please DO NOT delete this file!
# It is necessary for linking the library.

# The name that we can dlopen(3).
dlname='$sharedlibname'

# Names of this library.
library_names='$librarynames'

# The name of the static archive.
old_library='$staticlibname'

# Libraries that this one depends upon.
dependency_libs='$deplibs'

# Version information for $sharedlibname
current=$current
age=$age
revision=$revision

# Is this an already installed library?
installed=$installed

# Should we warn about portability when linking against -modules?
shouldnotlink=$shouldnotlink

# Files to dlopen/dlpreopen
dlopen=''
dlpreopen=''

# Directory that this library needs to be installed in:
libdir='$libdir'
EOF
;
}

# write a wrapper script for an executable so it can be executed within
# the build directory
sub write_prog_wrapper
{
	my $program = shift;

	open(my $pw, '>', $program) or die "cannot write $program: $!\n";
	print $pw <<EOF
#!/bin/sh

# $program - wrapper for $ltdir/$program
# Generated by libtool $version

argdir=`dirname \$0`
if test -f "\$argdir/$ltdir/$program"; then
    # Add our own library path to LD_LIBRARY_PATH
    LD_LIBRARY_PATH=\$argdir/$ltdir
    export LD_LIBRARY_PATH

    # Run the actual program with our arguments.
    exec "\$argdir/$ltdir/$program" \${1+"\$\@"}

    echo "\$0: cannot exec $program \${1+"\$\@"}"
    exit 1
else
    echo "\$0: error: \\\`\$argdir/$ltdir/$program' does not exist." 1>&2
    exit 1
fi
EOF
;
}

sub is_prog_wrapper
{
	my $program = shift;

	open(my $pw, '<', $program) or die "cannot open $program: $!\n";
	return eval(grep { m/wrapper\sfor/ } <$pw>);
}

sub parse_file
{
	my $filename = shift;
	my $info = shift;

	print "parsing $filename\n" if $D;
	open(my $fh, '<', $filename) or die "cannot read $filename: $!\n";
	while (<$fh>) {
		next if /^#/;
		next if /^\s*$/;
		if (m/^(\S+)='(.*)'$/) {
			$info->{$1} = $2;
		} elsif (m/^(\S+)=(\S+)$/) {
			$info->{$1} = $2;
		}
	}
}

# resolve .la files until a level with empty dependency_libs is reached.
sub resolve_la
{
	my $argstring = shift;

	my @args = split /\s+/, $argstring;
	print "resolve level: $res_level\n" if $D;

	foreach my $a (@args) {
		next if ($a !~ m/(.*)\.la$/);
		my %lainfo;
		if (!exists $file_parsed{$a} || !$file_parsed{$a}) {
			parse_file($a, \%lainfo);
			$file_parsed{$a} = 1;
			if (exists $lainfo{'dependency_libs'}) {
				$res_level++;
				$a = $a . ' ' . resolve_la($lainfo{'dependency_libs'});
				$res_level--;
				push @deplibs, $lainfo{'dependency_libs'};
			}
		} else {
			$a = '';
			next;
		}
	}
	return join ' ', @args;
}

# parse link flags and arguments
# eliminate all -L and -l flags in the argument string and add the
# corresponding directories and library names to the dirs/libs hashes.
# fill deplibs, to be taken up as dependencies in the resulting .la file...
# set up a hash for library files which haven't been found yet.
# deplibs are formed by collecting the original -L/-l flags, plus
# any .la files passed on the command line, EXCEPT when the .la file
# does not point to a shared library.
# pass 1 (la == 1)
# -Lfoo, -lfoo, foo.a, foo.la
# recursively find .la files corresponding to -l flags; if there is no .la
# file, just inspect the library file itself for any dependencies.
# pass 2 (la == 0)
# -Lfoo, -lfoo, foo.a
# no recursion in pass 2
# fill orderedlibs array, which is the sequence after resolving all .la
sub parse_linkargs
{
	my $argstring = shift;
	my $la = shift;

	my @args = split /\s+/, $argstring;
	print "parse_linkargs level: $parse_level\n" if $D;

	my $seen_pthread = 0;
	foreach my $a (@args) {
		if ($a eq '-lc') {	
			# don't link explicitly with libc (just remove -lc)
			$a = '';   
		} elsif ($a eq '-pthread' && !$seen_pthread) {	
			# XXX special treatment since it's not a -l flag
			push @deplibs, $a;
			$seen_pthread = 1;
		} elsif ($a && $a =~ m/^-L(.*)/) {
			if (!exists $dirs{$1}) {
				$dirs{$1} = 1;
				push @deplibs, $a;
			}
			$a = '';
		} elsif ($a && $a =~ m/^-R(.*)/) {
			# -R options coming from .la resolution
			# those from @ARGV are in @Ropts
			$a = "-Wl,-rpath,$1";
		} elsif ($a && $a =~ m/^-l(.*)/) {
			my $lstring = '';
			my $key = $1;
			if (!exists $libstofind{$key}) {
				$libstofind{$key} = 1;
				if ($la) {
					my $lafile = find_la($key);
					if ($lafile) {
						push @deplibs, $lafile;
						$a = $lafile;
						next;
					} else {
						my $libpath = find_lib($key, @libsearchdirs);
						# avoid searching again later
						$libs{$key} = $libpath if ($libpath);
						my @deps = inspect_lib($libpath);
						# push @deplibs, @deps;
						foreach my $d (@deps) {
							my $k = basename $d;
							$k =~ s/^(\S+)\.so.*$/$1/;
							$k =~ s/^lib//;
							$lstring .= "-l$k ";
						}
						push @deplibs, $a;
					}
				}
			}
			if ($la) {
				$parse_level++;
				$a .= ' ';
				$a .= parse_linkargs($lstring, $la) if ($lstring);
				$parse_level--;
			} else {
				push @orderedlibs, $key;
				$a = '';
			}
		} elsif ($a && $a =~ m/(\S+\/)*(\S+)\.a$/) {
			my $key = $2;
			$key =~ s/^lib//;
			$libs{$key} = $a;
			if (!$la) {
				push @orderedlibs, $key;
				$a = '';
			}
		} elsif ($a && $a =~ m/(\S+\/)*(\S+)\.la$/) {
			my $key = $2;
			$key =~ s/^lib//;
			my %lainfo;
			my $d = abs_path(dirname($a));
			$dirs{$d} = 1;
			my $fulla = abs_path($a);
			parse_file($fulla, \%lainfo);
			my $dlname = $lainfo{'dlname'};
			my $oldlib = $lainfo{'old_library'};
			if ($d !~ m/\Q$ltdir\E$/ && $lainfo{'installed'} eq 'no') {
				$d .= "/$ltdir";
			}
			if ($dlname && $la) {
				push @deplibs, $fulla;
			}
			# the following should happen only in pass 2
			next if ($la);
			push @orderedlibs, $key;
			$a = '';
			# get the name we need (this may include a -release)
			if (!$dlname && !$oldlib) {
				die "neither static nor shared library found in $a\n";
			}
			# XXX in some cases there are multiple libs with the same name
			# so probably need to use a different key
			if ($dlname eq '') {
				# static
				$libs{$key} = "$d/$oldlib";
			} else {
				# shared
				$libs{$key} = "$d/$dlname";
			}
			print "\$libs{$key} = ", $libs{$key}, "\n" if $D;
		}
	}
	return join ' ', @args;
}

# find .la file associated with a -llib flag
# XXX pick the right one if multiple are found!
sub find_la
{
	my $l = shift;

	# sort dir search order by priority
	# XXX not fully correct yet
	my @dirs = sort { $dirs{$b} <=> $dirs{$a} } keys %dirs;
	print "searching .la for $l ...\n" if $D;
	foreach my $d (@dirs) {
		print "   ... in $d\n" if $D;
		foreach my $la_candidate ("$d/lib$l.la", "$d/$l.a") {
			if (-f $la_candidate) {
				return $la_candidate;
			}
		}
	}
	print ".la for $l not found!\n" if $D;
	return 0;
}

# find actual library filename
# XXX pick the right one if multiple are found!
sub find_lib
{
	my $libtofind = shift;
	my @ldconfigdirs = @_;	# search there last

	my $libfile = 0;
	my @globbedlib;

	# sort dir search order by priority
	# XXX not fully correct yet
	my @dirs = sort { $dirs{$b} <=> $dirs{$a} } keys %dirs;
	push @dirs, @ldconfigdirs;
	print "searching for $libtofind ...\n" if $D;
	foreach my $d (@dirs) {
		my $sd = $d;
		# search in .libs when priority is high
		$sd = "$d/$ltdir" if (exists $dirs{$d} && $dirs{$d} > 3);
		print "   ... in $d\n" if $D;
		@globbedlib = glob <$sd/lib$libtofind.so.*>;
		if ($globbedlib[0]) {
			print "found $libtofind in $sd\n" if $D;
			$libfile = $globbedlib[0];
			last;
		} else {	# XXX find static library instead?
			if (-f "$sd/lib$libtofind.a") {
				print "found static $libtofind in $sd\n" if $D;
				$libfile = "$sd/lib$libtofind.a";
				last;
			}
		}
	}
	print "$libtofind not found!\n" if (!$libfile);
	return $libfile;
}

# give a list of library dependencies found in the actual shared library
sub inspect_lib
{
	my $filename = shift;

	my @deps;
	print "inspecting $filename for library dependencies...\n" if $D;
	open(my $fh, '-|', "objdump -p $filename");
	while (<$fh>) {
		if (m/\s+NEEDED\s+(\S+)\s*$/) {
			push @deps, $1;
		}
	}
	print "found ", (@deps == 0) ? 'no ' : '',
		"deps for $filename\n@deps\n" if $D;
	return @deps;
}

# prepare dependency_libs information for the .la file which is installed
# i.e. remove any .libs directories and use the final libdir for all the
# .la files
sub process_deplibs
{
	my $deplibline = shift;

	my @linkflags = split /\s+/, $deplibline;
	my %la_in_ldpath;

	foreach my $lf (@linkflags) {
		if ($lf =~ m/-L\S+\Q$ltdir\E$/) {
			$lf = '';
		} elsif ($lf =~ m/\/\S+\/(\S+\.la)/) {
			my $lafile = $1;
			my %lainfo;
			parse_file($lf, \%lainfo);
			$lf = $lainfo{'libdir'}.'/'.$lafile;
		}
	}
	return join ' ', @linkflags;
}

# construct linker command (list) for either libraries or programs
sub linkcmds
{
	my $la = shift;
	my $fname = shift;
	my $odir = shift;
	my $lmode = shift;	# LIBRARY or PROGRAM
	my $shared = shift;
	my $objs = shift;

	print "creating link command for ",
		($lmode == PROGRAM) ? "program" : "library", " (linked ",
		($shared) ? "dynam" : "stat", "ically)\n" if $D;
	
	my @libflags;
	my @cmdlist;
	my $cmd = '';
	my $dst = ($odir eq '.') ? "$ltdir/$fname" : "$odir/$ltdir/$fname";
	mkdir "$odir/$ltdir" if (! -d "$odir/$ltdir");

	my @argvcopy = @ARGV;
	my $argvstring = join ' ', @argvcopy;
	$argvstring = resolve_la($argvstring);
	@orderedlibs = ();
	$argvstring = parse_linkargs($argvstring, 0);
	@argvcopy = split /\s+/, $argvstring;
	print "orderedlibs = @orderedlibs\n" if $D;
	my @finalorderedlibs = reverse_zap_duplicates(@orderedlibs);
	print "final orderedlibs = @finalorderedlibs\n" if $D;

	# static linking
	if (!$shared) {
		my @libfiles = values %libs;
		if ($D) {
			my @dirval = keys %dirs;
			my @libval = keys %libs;
			print "dirs:  @dirval\n";
			print "libs:  @libval\n";
			print "libfiles: @libfiles\n";
		}
		if ($lmode == LIBRARY) {
			$cmd = "ar cru $dst";
			$cmd .= " @$objs" if (@$objs);
			foreach my $k (@finalorderedlibs) {
				my $a = $libs{$k};
				if ($a =~ m/\.a$/ && $a !~ m/_pic\.a/) {
					# extract objects from archive
					my $libfile = basename $a;
					my $xdir = "$odir/$ltdir/${la}x/$libfile";
					extract_archive($xdir, $a);
					my @kobjs = get_objlist_from_archive($a);
					map { $_ = "$xdir/$_"; } @kobjs;
					push @libflags, @kobjs;
				}
			}
			$cmd .= " @libflags" if (@libflags);
			push @cmdlist, $cmd;
			push @cmdlist, "ranlib $dst";
			return @cmdlist;
		} elsif ($lmode == PROGRAM) {
			die "static linking of programs not supported yet\n";
		}
	}

	# dynamic linking
	my @Rflags = @Ropts;
	map { $_ = "-Wl,-rpath,$_" } @Rflags;
	foreach my $l (keys %libstofind) {
		my $libpath = find_lib($l);
		$libs{$l} = $libpath if ($libpath);
	}

	my @libfiles = values %libs;
	if ($D) {
		my @dirval = keys %dirs;
		my @libval = keys %libs;
		print "dirs:  @dirval\n";
		print "libs:  @libval\n";
		print "libfiles: @libfiles\n";
	}

	create_symlinks($ltdir, \@libfiles);
	map { $_ = "$ltdir/". basename $_ } @libfiles;
	print "symlinks to libfiles used for linking: @libfiles\n" if $D;
	foreach my $k (@finalorderedlibs) {
		my $a = $libs{$k};
		if ($a =~ m/\.a$/) {
			# don't make a -lfoo out of a static library
			if ($lmode == LIBRARY) {
				# extract objects from archive
				my $libfile = basename $a;
				my $xdir = "$odir/$ltdir/${la}x/$libfile";
				extract_archive($xdir, $a);
				my @kobjs = get_objlist_from_archive($a);
				map { $_ = "$xdir/$_"; } @kobjs;
				push @libflags, @kobjs;
			} elsif ($lmode == PROGRAM) {
				push @libflags, $a;
			}
		} else {
			my $lib = basename $a;
			if ($lib =~ m/^lib(.*)\.so(\.\d+){2}/) {
				$lib = $1;
			} else {
				print "warning: cannot derive -l flag from library filename, assuming hash key\n";
				$lib = $k;
			}
			push @libflags, "-l$lib";
		}
	}

	$cmd = "$ltprog";
	$cmd .= " $sharedflag $picflags" if ($lmode == LIBRARY);
	$cmd .= " -o $dst";
	$cmd .= " @argvcopy";
	$cmd .= " @$objs" if (@$objs);
	$cmd .= " -L$ltdir @libflags" if (@libflags);
	$cmd .= " @Rflags" if (@Rflags);
	push @cmdlist, $cmd;

	return @cmdlist;
}

# populate arrays of non-pic and pic objects and remove these from @ARGV
sub generate_objlist
{
	my $objs = shift;
	my $sobjs = shift;
	my $allpic = 1;

	foreach my $a (@ARGV) {
		if ($a =~ m/\S+\.lo$/) {
			my %loinfo;
			my $ofile = basename $a;
			my $odir = dirname $a;
			parse_file($a, \%loinfo);
			if ($loinfo{'non_pic_object'}) {
				my $o;
				$o .= "$odir/" if ($odir ne '.');
				$o .= $loinfo{'non_pic_object'};
				push @$objs, $o;
			}
			if ($loinfo{'pic_object'}) {
				my $o;
				$o .= "$odir/" if ($odir ne '.');
				$o .= $loinfo{'pic_object'};
				push @$sobjs, $o;
			} else {
				$allpic = 0;
			}
			$a = '';
		}
	}
	return $allpic;
}

# XXX reuse code from SharedLibs.pm instead
sub get_search_dirs
{
	my @libsearchdirs;
	open(my $fh, '-|', 'ldconfig -r');
	if (defined $fh) {
		while (<$fh>) {
			if (m/^\s*search directories:\s*(.*?)\s*$/o) {
				foreach my $d (split(/\:/o, $1)) {
					push @libsearchdirs, $d;
				}
				last;
			}
		}
		close($fh);
	} else {
		die "Can't find ldconfig\n";
        }
	return @libsearchdirs;
}

sub extract_archive
{
	my $dir = shift;
	my $archive = shift;

	if (! -d $dir) {
		print "mkdir -p $dir\n" if $D;
		File::Path::mkpath($dir);
	}
	perform("cd $dir && ar x $archive");
}

sub get_objlist_from_archive
{
	my $a = shift;

	open(my $arh, '-|', "ar t $a");
	my @o = <$arh>;
	close $arh;
	map { chomp; } @o;
	return @o;
}

# walk a list from back to front, removing any duplicates
# this should make sure a library's dependencies are behind the library itself
sub reverse_zap_duplicates
{
	my @arglist = @_;

	my $h = {};
	my @r;
	for (my $i = $#arglist; $i >= 0; $i--) {
		my $el = $arglist[$i];
		next if (defined $h->{$el});
		unshift @r, $el;
		$h->{$el} = 1;
	}
	return @r;
}

# try to guess libtool mode when it is not specified
sub guess_implicit_mode
{
	my $m = 0;
	if ($ltprog =~ m/(install([.-]sh)?|cp)$/) {
		$m = 'install';
	} elsif ($ltprog =~ m/cc|c\+\+/) {	# XXX improve test
		if (grep { $_ eq '-c' } @ARGV) {
			$m = 'compile';
		} else {
			$m = 'link';
		}
	}
	return $m;	
}

# escape quotes and meta-characters
# protect elements containing whitespace or meta-characters by quotes
sub handle_special_chars
{
	my $a = shift;
	map { $_ =~ s,(['"]),\\$1,g; $_ = "\"$_\"" if $_ =~ m/[\s&()<>]/ } @$a;
}
