
# Copyright (c) 2021-2023, PostgreSQL Global Development Group

package Install;

#
# Package that provides 'make install' functionality for msvc builds
#
# src/tools/msvc/Install.pm
#
use strict;
use warnings;
use Carp;
use File::Basename;
use File::Copy;
use File::Find ();

use Exporter;
our (@ISA, @EXPORT_OK);
@ISA = qw(Exporter);
@EXPORT_OK = qw(Install);

my $insttype;
my @client_contribs = ('oid2name', 'pgbench', 'vacuumlo');
my @client_program_files = (
	'clusterdb', 'createdb', 'createuser', 'dropdb',
	'dropuser', 'ecpg', 'libecpg', 'libecpg_compat',
	'libpgtypes', 'libpq', 'pg_amcheck', 'pg_basebackup',
	'pg_config', 'pg_dump', 'pg_dumpall', 'pg_isready',
	'pg_receivewal', 'pg_recvlogical', 'pg_restore', 'psql',
	'reindexdb', 'vacuumdb', @client_contribs);

sub lcopy
{
	my $src = shift;
	my $target = shift;

	if (-f $target)
	{
		unlink $target || confess "Could not delete $target\n";
	}

	(my $retval = copy($src, $target))
	  || confess "Could not copy $src to $target\n";

	return $retval;
}

sub Install
{
	$| = 1;

	my $target = shift;
	$insttype = shift;
	$insttype = "all" unless ($insttype);

	# if called from vcregress, the config will be passed to us
	# so no need to re-include these
	our $config = shift;
	unless ($config)
	{

		# suppress warning about harmless redeclaration of $config
		no warnings 'misc';
		do "./config_default.pl";
		do "./config.pl" if (-f "config.pl");
	}

	# Move to the root path depending on the current location.
	if (-f "../../../configure")
	{
		chdir("../../..");
	}
	elsif (-f "../../../../configure")
	{
		chdir("../../../..");
	}

	my $conf = "";
	if (-d "debug")
	{
		$conf = "debug";
	}
	if (-d "release")
	{
		$conf = "release";
	}
	die "Could not find debug or release binaries" if ($conf eq "");
	my $majorver = DetermineMajorVersion();
	print "Installing version $majorver for $conf in $target\n";

	my @client_dirs = ('bin', 'lib', 'share', 'symbols');
	my @all_dirs = (
		@client_dirs, 'doc', 'doc/contrib', 'doc/extension', 'share/contrib',
		'share/extension', 'share/timezonesets', 'share/tsearch_data');
	if ($insttype eq "client")
	{
		EnsureDirectories($target, @client_dirs);
	}
	else
	{
		EnsureDirectories($target, @all_dirs);
	}

	CopySolutionOutput($conf, $target);
	my $sample_files = [];
	my @top_dir = ("src");
	@top_dir = ("src\\bin", "src\\interfaces") if ($insttype eq "client");
	File::Find::find(
		{
			wanted => sub {
				/^.*\.sample\z/s
				  && push(@$sample_files, $File::Find::name);

				# Don't find files of in-tree temporary installations.
				$_ eq 'share' and $File::Find::prune = 1;
			}
		},
		@top_dir);
	CopySetOfFiles('config files', $sample_files, $target . '/share/');
	CopyFiles(
		'Import libraries',
		$target . '/lib/',
		"$conf\\", "postgres\\postgres.lib", "libpgcommon\\libpgcommon.lib",
		"libpgport\\libpgport.lib");
	CopyContribFiles($config, $target);
	CopyIncludeFiles($target);

	if ($insttype ne "client")
	{
		CopySetOfFiles(
			'timezone names',
			[ glob('src\timezone\tznames\*.txt') ],
			$target . '/share/timezonesets/');
		CopyFiles(
			'timezone sets',
			$target . '/share/timezonesets/',
			'src/timezone/tznames/', 'Default', 'Australia', 'India');
		CopySetOfFiles(
			'BKI files',
			[ glob("src\\backend\\catalog\\postgres.*") ],
			$target . '/share/');
		CopySetOfFiles(
			'SQL files',
			[ glob("src\\backend\\catalog\\*.sql") ],
			$target . '/share/');
		CopyFiles(
			'Information schema data', $target . '/share/',
			'src/backend/catalog/', 'sql_features.txt');
		CopyFiles(
			'Error code data', $target . '/share/',
			'src/backend/utils/', 'errcodes.txt');
		GenerateTimezoneFiles($target, $conf);
		GenerateTsearchFiles($target);
		CopySetOfFiles(
			'Stopword files',
			[ glob("src\\backend\\snowball\\stopwords\\*.stop") ],
			$target . '/share/tsearch_data/');
		CopySetOfFiles(
			'Dictionaries sample files',
			[ glob("src\\backend\\tsearch\\dicts\\*_sample*") ],
			$target . '/share/tsearch_data/');

		my $pl_extension_files = [];
		my @pldirs = ('src/pl/plpgsql/src');
		push @pldirs, "src/pl/plperl" if $config->{perl};
		push @pldirs, "src/pl/plpython" if $config->{python};
		push @pldirs, "src/pl/tcl" if $config->{tcl};
		File::Find::find(
			{
				wanted => sub {
					/^(.*--.*\.sql|.*\.control)\z/s
					  && push(@$pl_extension_files, $File::Find::name);

					# Don't find files of in-tree temporary installations.
					$_ eq 'share' and $File::Find::prune = 1;
				}
			},
			@pldirs);
		CopySetOfFiles('PL Extension files',
			$pl_extension_files, $target . '/share/extension/');
	}

	GenerateNLSFiles($target, $config->{nls}, $majorver) if ($config->{nls});

	print "Installation complete.\n";
	return;
}

sub EnsureDirectories
{
	my $target = shift;
	mkdir $target unless -d ($target);
	while (my $d = shift)
	{
		mkdir $target . '/' . $d unless -d ($target . '/' . $d);
	}
	return;
}

sub CopyFiles
{
	my $what = shift;
	my $target = shift;
	my $basedir = shift;

	print "Copying $what";
	while (my $f = shift)
	{
		print ".";
		$f = $basedir . $f;
		die "No file $f\n" if (!-f $f);
		lcopy($f, $target . basename($f)) || croak "Could not copy $f: $!\n";
	}
	print "\n";
	return;
}

sub CopySetOfFiles
{
	my $what = shift;
	my $flist = shift;
	my $target = shift;
	print "Copying $what" if $what;
	foreach (@$flist)
	{
		my $tgt = $target . basename($_);
		print ".";
		lcopy($_, $tgt) || croak "Could not copy $_: $!\n";
	}
	print "\n";
	return;
}

sub CopySolutionOutput
{
	my $conf = shift;
	my $target = shift;
	my $rem =
	  qr{Project\("\{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942\}"\) = "([^"]+)"};

	my $sln = read_file("pgsql.sln") || croak "Could not open pgsql.sln\n";

	my $vcproj = 'vcproj';
	if ($sln =~
		/Microsoft Visual Studio Solution File, Format Version (\d+)\.\d+/
		&& $1 >= 11)
	{
		$vcproj = 'vcxproj';
	}

	print "Copying build output files...";
	while ($sln =~ $rem)
	{
		my $pf = $1;

		# Hash-of-arrays listing where to install things.  For each
		# subdirectory there's a hash key, and the value is an array
		# of file extensions to install in that subdirectory.  Example:
		# { 'bin' => [ 'dll', 'lib' ],
		#   'lib' => [ 'lib' ] }
		my %install_list;
		my $is_sharedlib = 0;

		$sln =~ s/$rem//;

		next
		  if ($insttype eq "client" && !grep { $_ eq $pf }
			@client_program_files);

		my $proj = read_file("$pf.$vcproj")
		  || croak "Could not open $pf.$vcproj\n";

		# Check if this project uses a shared library by looking if
		# SO_MAJOR_VERSION is defined in its Makefile, whose path
		# can be found using the resource file of this project.
		if ((      $vcproj eq 'vcxproj'
				&& $proj =~ qr{ResourceCompile\s*Include="([^"]+)"})
			|| (   $vcproj eq 'vcproj'
				&& $proj =~ qr{File\s*RelativePath="([^\"]+)\.rc"}))
		{
			my $projpath = dirname($1);
			my $mfname =
			  -e "$projpath/GNUmakefile"
			  ? "$projpath/GNUmakefile"
			  : "$projpath/Makefile";
			my $mf = read_file($mfname) || croak "Could not open $mfname\n";

			$is_sharedlib = 1 if ($mf =~ /^SO_MAJOR_VERSION\s*=\s*(.*)$/mg);
		}

		if ($vcproj eq 'vcproj' && $proj =~ qr{ConfigurationType="([^"]+)"})
		{
			if ($1 == 1)
			{
				push(@{ $install_list{'bin'} }, "exe");
			}
			elsif ($1 == 2)
			{
				push(@{ $install_list{'lib'} }, "dll");
				if ($is_sharedlib)
				{
					push(@{ $install_list{'bin'} }, "dll");
					push(@{ $install_list{'lib'} }, "lib");
				}
			}
			else
			{

				# Static libraries, such as libpgport, only used internally
				# during build, don't install.
				next;
			}
		}
		elsif ($vcproj eq 'vcxproj'
			&& $proj =~ qr{<ConfigurationType>(\w+)</ConfigurationType>})
		{
			if ($1 eq 'Application')
			{
				push(@{ $install_list{'bin'} }, "exe");
			}
			elsif ($1 eq 'DynamicLibrary')
			{
				push(@{ $install_list{'lib'} }, "dll");
				if ($is_sharedlib)
				{
					push(@{ $install_list{'bin'} }, "dll");
					push(@{ $install_list{'lib'} }, "lib");
				}
			}
			else    # 'StaticLibrary'
			{

				# Static lib, such as libpgport, only used internally
				# during build, don't install.
				next;
			}
		}
		else
		{
			croak "Could not parse $pf.$vcproj\n";
		}

		# Install each element
		foreach my $dir (keys %install_list)
		{
			foreach my $ext (@{ $install_list{$dir} })
			{
				lcopy("$conf\\$pf\\$pf.$ext", "$target\\$dir\\$pf.$ext")
				  || croak "Could not copy $pf.$ext\n";
			}
		}
		lcopy("$conf\\$pf\\$pf.pdb", "$target\\symbols\\$pf.pdb")
		  || croak "Could not copy $pf.pdb\n";
		print ".";
	}
	print "\n";
	return;
}

sub GenerateTimezoneFiles
{
	my $target = shift;
	my $conf = shift;
	my $mf = read_file("src/timezone/Makefile");
	$mf =~ s{\\\r?\n}{}g;

	$mf =~ /^TZDATAFILES\s*:?=\s*(.*)$/m
	  || die "Could not find TZDATAFILES line in timezone makefile\n";
	my @tzfiles = split /\s+/, $1;

	print "Generating timezone files...";

	my @args = ("$conf/zic/zic", '-d', "$target/share/timezone");
	foreach (@tzfiles)
	{
		my $tzfile = $_;
		$tzfile =~ s|\$\(srcdir\)|src/timezone|;
		push(@args, $tzfile);
	}

	system(@args);
	print "\n";
	return;
}

sub GenerateTsearchFiles
{
	my $target = shift;

	print "Generating tsearch script...";
	system(
		'perl', 'src/backend/snowball/snowball_create.pl',
		'--input', 'src/backend/snowball/',
		'--outdir', "$target/share/");
	print "\n";
	return;
}

sub CopyContribFiles
{
	my $config = shift;
	my $target = shift;

	print "Copying contrib data files...";
	foreach my $subdir ('contrib', 'src/test/modules')
	{
		my $D;
		opendir($D, $subdir) || croak "Could not opendir on $subdir!\n";
		while (my $d = readdir($D))
		{
			# These configuration-based exclusions must match vcregress.pl
			next if ($d eq "uuid-ossp" && !defined($config->{uuid}));
			next if ($d eq "sslinfo" && !defined($config->{openssl}));
			next if ($d eq "pgcrypto" && !defined($config->{openssl}));
			next if ($d eq "xml2" && !defined($config->{xml}));
			next if ($d =~ /_plperl$/ && !defined($config->{perl}));
			next if ($d =~ /_plpython$/ && !defined($config->{python}));
			next if ($d eq "sepgsql");

			CopySubdirFiles($subdir, $d, $config, $target);
		}
	}
	print "\n";
	return;
}

sub CopySubdirFiles
{
	my $subdir = shift;
	my $module = shift;
	my $config = shift;
	my $target = shift;

	return if ($module =~ /^\./);
	return unless (-f "$subdir/$module/Makefile");
	return
	  if ($insttype eq "client" && !grep { $_ eq $module } @client_contribs);

	my $mf = read_file("$subdir/$module/Makefile");
	$mf =~ s{\\\r?\n}{}g;

	# Note: we currently don't support setting MODULEDIR in the makefile
	my $moduledir = 'contrib';

	my $flist = '';
	if ($mf =~ /^EXTENSION\s*=\s*(.*)$/m) { $flist .= $1 }
	if ($flist ne '')
	{
		$moduledir = 'extension';
		$flist = ParseAndCleanRule($flist, $mf);

		foreach my $f (split /\s+/, $flist)
		{
			lcopy("$subdir/$module/$f.control",
				"$target/share/extension/$f.control")
			  || croak("Could not copy file $f.control in contrib $module");
			print '.';
		}
	}

	$flist = '';
	if ($mf =~ /^DATA_built\s*=\s*(.*)$/m) { $flist .= $1 }
	if ($mf =~ /^DATA\s*=\s*(.*)$/m)       { $flist .= " $1" }
	$flist =~ s/^\s*//;    # Remove leading spaces if we had only DATA_built

	if ($flist ne '')
	{
		$flist = ParseAndCleanRule($flist, $mf);

		foreach my $f (split /\s+/, $flist)
		{
			lcopy("$subdir/$module/$f",
				"$target/share/$moduledir/" . basename($f))
			  || croak("Could not copy file $f in contrib $module");
			print '.';
		}
	}

	$flist = '';
	if ($mf =~ /^DATA_TSEARCH\s*=\s*(.*)$/m) { $flist .= $1 }
	if ($flist ne '')
	{
		$flist = ParseAndCleanRule($flist, $mf);

		foreach my $f (split /\s+/, $flist)
		{
			lcopy("$subdir/$module/$f",
				"$target/share/tsearch_data/" . basename($f))
			  || croak("Could not copy file $f in $subdir $module");
			print '.';
		}
	}

	{
		$flist = '';
		if ($mf =~ /^HEADERS\s*=\s*(.*)$/m) { $flist .= $1 }
		my @modlist = ();
		my %fmodlist = ();
		while ($mf =~ /^HEADERS_([^\s=]+)\s*=\s*(.*)$/mg)
		{
			$fmodlist{$1} .= $2;
		}

		if ($mf =~ /^MODULE_big\s*=\s*(.*)$/m)
		{
			push @modlist, $1;
			if ($flist ne '')
			{
				$fmodlist{$1} = $flist;
				$flist = '';
			}
		}
		elsif ($mf =~ /^MODULES\s*=\s*(.*)$/m)
		{
			push @modlist, split /\s+/, $1;
		}

		croak "HEADERS requires MODULE_big in $subdir $module"
		  if $flist ne '';

		foreach my $mod (keys %fmodlist)
		{
			croak "HEADERS_$mod for unknown module in $subdir $module"
			  unless grep { $_ eq $mod } @modlist;
			$flist = ParseAndCleanRule($fmodlist{$mod}, $mf);
			EnsureDirectories($target, "include", "include/server",
				"include/server/$moduledir",
				"include/server/$moduledir/$mod");
			foreach my $f (split /\s+/, $flist)
			{
				lcopy("$subdir/$module/$f",
					"$target/include/server/$moduledir/$mod/" . basename($f))
				  || croak("Could not copy file $f in $subdir $module");
				print '.';
			}
		}
	}

	$flist = '';
	if ($mf =~ /^DOCS\s*=\s*(.*)$/mg) { $flist .= $1 }
	if ($flist ne '')
	{
		$flist = ParseAndCleanRule($flist, $mf);

		# Special case for contrib/spi
		$flist =
		  "autoinc.example insert_username.example moddatetime.example refint.example"
		  if ($module eq 'spi');
		foreach my $f (split /\s+/, $flist)
		{
			lcopy("$subdir/$module/$f", "$target/doc/$moduledir/$f")
			  || croak("Could not copy file $f in contrib $module");
			print '.';
		}
	}
	return;
}

sub ParseAndCleanRule
{
	my $flist = shift;
	my $mf = shift;

	# Strip out $(addsuffix) rules
	if (index($flist, '$(addsuffix ') >= 0)
	{
		my $pcount = 0;
		my $i;
		for (
			$i = index($flist, '$(addsuffix ') + 12;
			$i < length($flist);
			$i++)
		{
			$pcount++ if (substr($flist, $i, 1) eq '(');
			$pcount-- if (substr($flist, $i, 1) eq ')');
			last if ($pcount < 0);
		}
		$flist =
			substr($flist, 0, index($flist, '$(addsuffix '))
		  . substr($flist, $i + 1);
	}
	return $flist;
}

sub CopyIncludeFiles
{
	my $target = shift;

	EnsureDirectories($target, 'include', 'include/libpq', 'include/internal',
		'include/internal/libpq', 'include/server', 'include/server/parser');

	CopyFiles(
		'Public headers', $target . '/include/',
		'src/include/', 'postgres_ext.h',
		'pg_config.h', 'pg_config_ext.h',
		'pg_config_os.h', 'pg_config_manual.h');
	lcopy('src/include/libpq/libpq-fs.h', $target . '/include/libpq/')
	  || croak 'Could not copy libpq-fs.h';

	CopyFiles(
		'Libpq headers',
		$target . '/include/',
		'src/interfaces/libpq/', 'libpq-fe.h', 'libpq-events.h');
	CopyFiles(
		'Libpq internal headers',
		$target . '/include/internal/',
		'src/interfaces/libpq/', 'libpq-int.h', 'fe-auth-sasl.h',
		'pqexpbuffer.h');

	CopyFiles(
		'Internal headers',
		$target . '/include/internal/',
		'src/include/', 'c.h', 'port.h', 'postgres_fe.h');
	lcopy('src/include/libpq/pqcomm.h', $target . '/include/internal/libpq/')
	  || croak 'Could not copy pqcomm.h';

	CopyFiles(
		'Server headers',
		$target . '/include/server/',
		'src/include/', 'pg_config.h', 'pg_config_ext.h', 'pg_config_os.h');
	CopySetOfFiles(
		'',
		[ glob("src\\include\\*.h") ],
		$target . '/include/server/');
	my $D;
	opendir($D, 'src/include') || croak "Could not opendir on src/include!\n";

	CopyFiles(
		'PL/pgSQL header',
		$target . '/include/server/',
		'src/pl/plpgsql/src/', 'plpgsql.h');

	# some xcopy progs don't like mixed slash style paths
	(my $ctarget = $target) =~ s!/!\\!g;
	while (my $d = readdir($D))
	{
		next if ($d =~ /^\./);
		next if ($d eq '.git');
		next if ($d eq 'CVS');
		next unless (-d "src/include/$d");

		EnsureDirectories("$target/include/server/$d");
		my @args = (
			'xcopy', '/s', '/i', '/q', '/r', '/y', "src\\include\\$d\\*.h",
			"$ctarget\\include\\server\\$d\\");
		system(@args) && croak("Failed to copy include directory $d\n");
	}
	closedir($D);

	my $mf = read_file('src/interfaces/ecpg/include/Makefile');
	$mf =~ s{\\\r?\n}{}g;
	$mf =~ /^ecpg_headers\s*=\s*(.*)$/m
	  || croak "Could not find ecpg_headers line\n";
	CopyFiles(
		'ECPG headers',
		$target . '/include/',
		'src/interfaces/ecpg/include/',
		'ecpg_config.h', split /\s+/, $1);
	$mf =~ /^informix_headers\s*=\s*(.*)$/m
	  || croak "Could not find informix_headers line\n";
	EnsureDirectories($target . '/include', 'informix', 'informix/esql');
	CopyFiles(
		'ECPG informix headers',
		$target . '/include/informix/esql/',
		'src/interfaces/ecpg/include/',
		split /\s+/, $1);
	return;
}

sub GenerateNLSFiles
{
	my $target = shift;
	my $nlspath = shift;
	my $majorver = shift;

	print "Installing NLS files...";
	EnsureDirectories($target, "share/locale");
	my @flist;
	File::Find::find(
		{
			wanted => sub {
				/^nls\.mk\z/s
				  && !push(@flist, $File::Find::name);
			}
		},
		"src");
	foreach (@flist)
	{
		my $prgm = DetermineCatalogName($_);
		s/nls.mk/po/;
		my $dir = $_;
		next unless ($dir =~ /([^\/]+)\/po$/);
		foreach (glob("$dir/*.po"))
		{
			my $lang;
			next unless /([^\/]+)\.po/;
			$lang = $1;

			EnsureDirectories($target, "share/locale/$lang",
				"share/locale/$lang/LC_MESSAGES");
			my @args = (
				"$nlspath\\bin\\msgfmt",
				'-o',
				"$target\\share\\locale\\$lang\\LC_MESSAGES\\$prgm-$majorver.mo",
				$_);
			system(@args) && croak("Could not run msgfmt on $dir\\$_");
			print ".";
		}
	}
	print "\n";
	return;
}

sub DetermineMajorVersion
{
	my $f = read_file('src/include/pg_config.h')
	  || croak 'Could not open pg_config.h';
	$f =~ /^#define\s+PG_MAJORVERSION\s+"([^"]+)"/m
	  || croak 'Could not determine major version';
	return $1;
}

sub DetermineCatalogName
{
	my $filename = shift;

	my $f = read_file($filename) || croak "Could not open $filename";
	$f =~ /CATALOG_NAME\s*\:?=\s*(\S+)/m
	  || croak "Could not determine catalog name in $filename";
	return $1;
}

sub read_file
{
	my $filename = shift;
	my $F;
	local $/ = undef;
	open($F, '<', $filename) || die "Could not open file $filename\n";
	my $txt = <$F>;
	close($F);

	return $txt;
}

1;
