#!perl -w

use strict;
use locale;
use File::Find;
use Getopt::Long;

use vars qw( 
	$opt_d $opt_u $opt_f $opt_i @ignores @includes @tmp @oldargs
	%used_includes $un $re_scripts_ext $first_subsect $cur_subsec
);

#==============================================================================

=head1 NAME

	scripts.nsh.pl - генерация NSIS-сценария установки/деинсталляции скриптов для OpenConf

=head1 SYNOPSIS

perl scripts.nsh.pl -d D:\home\KuntashovAM\projects\OpenConf_Scripts\Скрипты > scripts.nsh

perl scripts.nsh.pl -d C:\1Cv77\bin\config\scripts -f filters.txt -i scripts > scripts.nsh

perl scripts.nsh.pl -u -d C:\1Cv77\bin\config\scripts -f filters.txt -i scripts >> scripts.nsh
	
=head1 DESCRIPTION

	Скрипт предназначен для автоматической генерации NSIS-сценариев установки скриптов 
для OpenConf'а на основе структуры каталога, в котором располагаются эти скрипты.

	В качестве такого каталога (назовем его исходным) имеет смысл использовать каталог 
scripts эталонной инсталляции OpenConf (на основе которой генерируется инсталлятор), 
либо каталог Скрипты рабочей копии модуля OpenConf_Scripts репозитория cvs.alterplast.ru.

	Весь генерируемый код сценариев выводится в stdin. Для сохранения сгенерированного
кода в файл следует использовать стандартные команды перенаправления вывода в файл (> и >>).

	Сценарий генерируется следующим образом: рекурсивно обходится исходный каталог,
и для каждого элемента каталога (подкаталога или файла) вызывается функция, которая 
генерирует часть сценария, соответствующую этому элементу.
	
	Если очередной элемент (под)каталога - подкаталог, то генерируется код субсекции с
таким же именем, как и имя этого подкаталога. Если очередной элемент - это файл, то 
генерируется код секции со сценарием установки этого файла. В результате получаем 
NSIS-сценарий такой структуры:

	;; ...
	SubSection "ВерсионныйКонтроль"
		Section "cvs.vbs"
			SectionIn 1
			!insertmacro OC_STATUS "Скрипты | cvs.vbs"
			SetOutPath "$INSTDIR\config\scripts\ВерсионныйКонтроль"
			File "C:\1Cv77\bin\config\scripts\ВерсионныйКонтроль\cvs.vbs"		
		SectionEnd ;; "cvs.vbs"
	
		;; ... какие-то другие секции ...
		
		Section "СравнитьОбъект.vbs"
			SectionIn 1
			!insertmacro OC_STATUS "Скрипты | СравнитьОбъект.vbs"
			SetOutPath "$INSTDIR\config\scripts\ВерсионныйКонтроль"
			File "C:\1Cv77\bin\config\scripts\ВерсионныйКонтроль\СравнитьОбъект.vbs"
		SectionEnd ;; "СравнитьОбъект.vbs"	
	SubSectionEnd
	;; ...

	При запуске скрипта с параметром -u будет сгенерирован сценарий деинсталляции с
аналогичной струкрурой секций (FIXME на самом деле здесь иерархия не важна, и возможно,
этот момент будет пересмотрен в пользу генерации единой "плоской" секции деинсталляции 
скриптов).
	
	Файлы скриптов идентифицируются по расширению (см. инициализацию переменной 
$re_scripts_ext ниже по коду), все остальные файлы игнорируются и в сценарий установки
не попадают. Также игнорируются служебные директории CVS.

	Для того, чтобы исключить попадание какого-либо скрипта в сценарий установки (а значит
и в сам инсталлятор), предусмотрена поддержка игнор-фильтров в виде регулярных выражений, 
применяемых к имени обрабатываемого файла. Если регулярное выражение матчит имя файла,
то такой файл пропускается (т.е. не попадает в сценарий).

	Фильтры должны быть записаны в текстовый файл, каждый фильтр располагается на новой 
строке следующего формата:

	ignore:регулярное_выражение

Например:

	ignore:common.vbs$
	ignore:Замена кода trad.vbs$
	
Слово ignore обязательно, перед ним не должно быть пробелов. Для использования файла
фильтров, путь к нему надо указать в качестве значения опции -f.

	Данный скрипт генерирует тривиальные сценарии установки скриптов, т.е. сценарии, банально
копирующие во время инсталляции скрипты в целевые каталоги с повторением структуры исходного 
каталога.

	В большинстве случаев такой подход работает, поскольку основная масса скриптов представлена
одним файлом и не использует никаких других дополнительных файлов.

	Для остальных случаев предусмотрен следующий подход. Для "сложных" в установке скриптов
(например, таких как Intellisence.vbs, который требует дополнительно установки *.ints-файлов)
пишутся отдельные сценарии и располагаются где-нибудь в одном месте. Имя файла сценария
должно совпадать с именем файла скрипта и иметь расширение nsh.

	В файл с игнор-фильтрами добавляются дополнительно фильтры аналогичного вида, только
вместо слова ignore используется слово include:

	include:регулярное_выражение

Например:
	
	include:\WIntellisence.vbs$
	
	Как и в случае с игнор-фильтрами каждое имя файла из целевой директории проверяется на
соответсвие include-фильтрам и в случае положительного исхода (один из фильтров матчит
имя) вместо сценария установки генерируется директива включения внешнего файла сценария
(!include), имя включаемого сценария берется таким же, как и имя обрабатываемого файла - для 
сценариев установки, или же "un.ИмяФайлаСкрипта" - для сценариев деинсталляции (т.е. добавляется
префикс "un."):

	!include "Intellisence.nsh

или 

	!include "un.Intellisence.nsh"

		Можно явно задать путь к каталогу с инклюд-файлами, который будет добавляться
к имени файла для включения. Для этого предусмотрена опция -i, в качестве значения которой
следует указать путь к каталогу с включаемыми сценариями.

	Игнор- и инклюд- фильтры в файле могут располагаться в произвольном порядке.
	
=cut

#==============================================================================

@oldargs = @ARGV;

$opt_u = $opt_d = $opt_f = $opt_i = "";

GetOptions('u'=>\$opt_u, 'd=s'=>\$opt_d, 'f=s'=>\$opt_f, 'i=s'=>\$opt_i);

$opt_d or die <<USAGE;
Usage: perl $0 [-u] -d path\\to\\dir [-f filters_list] [-i include_dir]
Parameters:
  -u              generate uninstall sections (add "un."-prefix to the section name)
  -f filter_list  use filters from specified filter_list file
  -i include_dir  include dir where *.nsh files are placed
USAGE

# избавляемся от слеша в конце пути, если он есть
$opt_d =~ s/[\/\\]$//;

-d $opt_d or die "Specified dir does not exists: $opt_d\n";
 
if ($opt_f) {

	-f $opt_f or die "Specified filter file (-f) not found: $opt_f";

	open FILTERS, "<$opt_f" or die("Can't open filter file $opt_f: $!");
	@tmp = (<FILTERS>);
	close FILTERS;
	
	@ignores	= grep s/^ignore:(.+?)\r*\n*$/$1/, @tmp;
	@includes	= grep s/^include:(.+?)\r*\n*$/$1/, @tmp;
	
	@tmp = ();
}

if ($opt_i) { 
	$opt_i =~ s/\//\\/;
	$opt_i .= "\\" unless $opt_i =~ /\\$/;
}

$un = $opt_u ? "un." : "";

$cur_subsec = "";
$re_scripts_ext = '\.(vbs|js|pl|pls|pys)$';

#==============================================================================

print <<HEADER;
;===========================================================================
;		СГЕНЕРИРОВАНО АВТОМАТИЧЕСКИ с помощью команды
; 			perl $0 @oldargs
; 		При следующей генерации ВСЕ ИЗМЕНЕНИЯ БУДУТ ПОТЕРЯНЫ!
; 		Изменения следует вносить в файл $0.
;===========================================================================

HEADER

# поехали... 
find (\&gen_nsh, $opt_d);

# закроем последнюю субсекцию

if ($un) { 
	# в субсекцию деинсталлера добавляем секцию для 
	# удаления соответствующей субсекции
	print <<SEC_RMDIR;
	Section "un.AfterUnInstall$cur_subsec"
		RMDir "\$INSTDIR\\config\\scripts\\$cur_subsec"
	SectionEnd
SEC_RMDIR
}

print qq(SubSectionEnd ;; $cur_subsec\n) if $cur_subsec;

#==============================================================================

sub gen_nsh {
	
	# директории CVS нас заведомо не интересуют
	!/(\.|CVS)$/ or return;
	$File::Find::dir !~ /CVS$/ or return;
	
	# если в $_ - имя директории, то закрываем предыдущую
	# субсекцию и открываем следующую
	if (-d) {
		if ($cur_subsec) {
			if ($un) { 
				# в субсекцию деинсталлера добавляем секцию для 
				# удаления соответствующей субсекции
				print <<SEC_RMDIR;
	Section "un.AfterUnInstall$cur_subsec"
		RMDir "\$INSTDIR\\config\\scripts\\$cur_subsec"
	SectionEnd
SEC_RMDIR
			}
			print qq(SubSectionEnd ;; $un$cur_subsec);
		}
		print qq(\nSubSection $un"$_"\n);
		$cur_subsec = $_;
		return;
	}

	# файл - есть скрипт?
	-f && /$re_scripts_ext/ or return;	

	# нам нужен правильный виндузный путь (везде обратные слеши)
	$File::Find::name =~ s/\//\\/g; 
	
	# если имя файла матчится хотя бы одним игнор-фильтром, то пропускаем его
	my @ret = grep { $File::Find::name =~ /$_/i } @ignores;
	return if scalar(@ret);

	# для файла существует отдельно написанный сценарий инсталляции?
	@ret = grep { $File::Find::name =~ /$_/i } @includes;
	if (scalar(@ret)) {
		# да, тогда просто генерируем код для его включения
		my $fname = (/(.+)\.(\w+)$/)[0];
		print qq(\t!include "$opt_i$un$fname.nsh"\n);
		return;
	}
	
	# отдельного сценария не предусмотрено, поэтому генерируем 
	# тривиальный сценарий самостоятельно
	if (!$opt_u) {
		print <<SECTION;
	Section "$_"
		SectionIn 1 2
		!insertmacro OC_STATUS "Установка скриптов | $_ ..."
		SetOutPath "\$INSTDIR\\config\\scripts\\$cur_subsec"
		File "$File::Find::name"
	SectionEnd ;; $_
SECTION
;
	} 
	else {
		print <<SECTION;
	Section "un.$_"
		!insertmacro OC_STATUS "Удаление скриптов | $_ ..."
		Delete "\$INSTDIR\\config\\scripts\\$cur_subsec\\$_"
	SectionEnd ;; un.$_
SECTION
	} # if

} # &gen_nsh

#==============================================================================
