#!/bin/perl -w

# Copyright (c) 2004-2009 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved.
# This component and the accompanying materials are made available
# under the terms of the License "Eclipse Public License v1.0"
# which accompanies this distribution, and is available
# at the URL "http://www.eclipse.org/legal/epl-v10.html".
#
# Initial Contributors:
# Nokia Corporation - initial contribution.
#
# Contributors:
#
# Description:
# distillsrc.pm - compiles a list of source used in .mrp files, and deletes
# any unused source
# 
#

package CDistillSrc;

use strict;
use File::Spec;
use File::Path;
use File::Basename;
use FindBin;
use lib $FindBin::Bin;
use ReadMrp;

use lib File::Spec->catdir($FindBin::Bin, '..', 'makecbr');
use CConfig;



# Constructor
#
# Parameters:
#
# $aSrcRoot : The root from which all src statements are based
# $aSrcPath : The path under aSrcRoot to the source tree to be processed
# $aSrcPrefix : An optional prefix which can be stripped from all src statements
# $aPlatform : e.g 'beech' - used to locate the platform specific product directory
#
# Returns: The object (or undef if there was a problem)
#
sub New($$$$)
	{
	my $proto = shift;
	my ($aSrcRoot, $aSrcPath, $aSrcPrefix, $aPlatform, $aCheckCase) = @_;

	my $class = ref($proto) || $proto;

	my $self = {};
	bless($self, $class);

	my $error = 0;

	if (!defined($aSrcRoot))
		{
		print "ERROR: RealTimeBuild: A srcroot must be given, to specify where all 'source' declarations originate from\n";
		$error = 1;
		}
		
	if (!defined($aSrcPath))
		{
		print "ERROR: RealTimeBuild: A srcpath must be given, to specify which source under the srcroot is to be filtered. Use '\\' to filter the entire srcroot\n";
		$error = 1;
		}

	if (!defined($aPlatform))
		{
		print "ERROR: RealTimeBuild: A platform must be given, to locate the product directory\n";
		$error = 1;
		}
		
	if ($error)
		{
		print "\n";
		}
	else
		{
		if ($aSrcPath =~ /\.\./)
			{
			print "ERROR: RealTimeBuild: The source path must be relative to the srcroot, and must not contain '..'\n";
			$error = 1;
			}
	
		$self->iSrcRoot($aSrcRoot);
		$self->iSrcPath($aSrcPath);
		$self->iSrcPrefix($aSrcPrefix);
		$self->iPlatform($aPlatform);
		$self->iSrcItems({});
		$self->iCheckCase(!!$aCheckCase);

		$self->AddSrcItem("os/buildtools/toolsandutils/productionbldtools/unref/orphan/cedprd/SuppKit", "non-shipped");
		$self->AddSrcItem("os/buildtools/toolsandutils/productionbldtools/unref/orphan/cedprd/tools", "non-shipped");
		$self->AddSrcItem("os/buildtools/toolsandutils/productionbldtools/unref/orphan/cedprd/DevKit", "non-shipped");
		$self->AddSrcItem("os/buildtools/toolsandutils/productionbldtools", "non-shipped");
		}

	if ($error)
		{
		$self = undef;
		}

	return $self;
	}

# Object data
#
sub iSrcRoot()
	{
	my $self = shift;
	if (@_) { $self->{iSRCROOT} = shift; }
	return $self->{iSRCROOT};
	}

sub iSrcPath()
	{
	my $self = shift;
	if (@_) { $self->{iSRCPATH} = shift; }
	return $self->{iSRCPATH};
	}

sub iSrcPrefix()
	{
	my $self = shift;
	if (@_) { $self->{iSRCPREFIX} = shift; }
	return $self->{iSRCPREFIX};
	}

sub iPlatform()
	{
	my $self = shift;
	if (@_) { $self->{iPLATFORM} = shift; }
	return $self->{iPLATFORM};
	}
	
sub iSrcItems()
	{
	my $self = shift;
	if (@_) { $self->{iSRCITEMS} = shift; }
	return $self->{iSRCITEMS};
	}

sub iCheckCase()
	{
	my $self = shift;
	if (@_) { $self->{iCHECKCASE} = shift; }
	return $self->{iCHECKCASE};
	}

sub iCorrectedCase()
	{
	my $self = shift;
	if (@_) { $self->{iCORRECTEDCASE} = shift; }
	return $self->{iCORRECTEDCASE};
	}

# LoadMrps - Records the source lines out of all .mrp files
#
# Parameters:
# $aConfig - optional configuration file, as used by makecbr
# $aLists - optional component lists, as used by makecbr
# $aMrps - optional .mrp files
#
# Returns: True, if the load was successful. False otherwise
#
sub LoadMrps($$$)
	{
	my $self = shift;
	my ($aConfig, $aLists, $aMrps) = @_;
	# Load in config file

	my @lists = @$aLists;
	my @mrps;
	foreach my $mrp (@$aMrps){
		{
		push @mrps, [$mrp, ''];
		}
	}
	my @configMrps = ();
    if (defined($aConfig))
		{
		my @configs = $self->_LoadConfig($aConfig);

		# Add mrps and lists (after planting them in srcroot)
		push @lists, map($self->_PlantFile($_), @{$configs[0]});
		@configMrps = map($self->_PlantFile($_), @{$configs[1]});
		foreach my $mrp (@configMrps)
			{
			push @mrps, [$mrp, ''];
			}
		}
	
	# Load in mrp lists
	foreach my $file (@lists)
		{
		if (open (MRPLIST, $file))
			{
			foreach my $line (<MRPLIST>)
				{
				chomp $line;
				$line =~ s/#.*$//; # Remove comments
				$line =~ s/^\s*//; # Remove extraneous spaces
				$line =~ s/\s*$//;
	
				if ($line ne "")
					{
					my @parms = split(/\s+/, $line);
	
					if (scalar(@parms) != 2)
						{
						warn "ERROR: RealTimeBuild: Entries in component list '$file' should be of the form 'name mrp_location'. Problem in line: $line\n";
						next;
						}
					else
						{
						# Ignore *nosource* entries
						next if ($parms[1] eq '*nosource*');
						
						push @mrps, [$self->_PlantFile($parms[1]), $parms[0]];
						}
					}
				}
			close MRPLIST or warn "ERROR: RealTimeBuild: Couldn't close '$file' : $!\n";
			}
		else
			{
			warn "Couldn't open '$file' : $!\n";	
			}
		}

	# Load all .mrp files
	if (scalar(@mrps) == 0)
		{
		die "ERROR: RealTimeBuild: No .mrp files were specified\n";
		}

	my $loaded = 1;
	
	foreach my $mrp (@mrps)
		{
		# Get path of mrp file (from here)
		my ($name, $path) = fileparse($mrp->[0]);
		# Convert to path from source root
		if (!($self->_RemoveBaseFromPath($self->iSrcRoot(), \$path)))
			{
			warn "ERROR: Mrp file $mrp->[0] isn't under the source root (".$self->iSrcRoot().")\n";
			next;
			}
		
		my $mrpobj;
        
        # To indicate the correct case and where the .mrp file comes from if failed to check letter case
        if (!($self->_CheckCase($mrp->[0]))) {
            my $mrp_error_source = "optional component list(by -f) or optional .mrp list(by -m)";
            foreach my $myName (@configMrps) {
                if ($myName eq $mrp->[0]) {
                    $mrp_error_source = "config file '".$aConfig."'";
                    last;
                }
            } 
            print "WARNING: Case of '".$mrp->[0]."' supplied in ".$mrp_error_source." does not match the file system. Should be ".$self->iCorrectedCase()."\n";
        }
        
		if (!eval { $mrpobj = New ReadMrp($mrp->[0]) })
			{
			$loaded = 0;
			my $message = $@;
			$message =~ s/^(ERROR:\s*)?/ERROR: RealTimeBuild: /i;
			print $message;
			}
		else
			{
			my $selfowned = 0;
			my $mrpComponentName = $mrpobj->GetComponent();
			if( ($mrp->[1] ne '') && (lc($mrp->[1]) ne lc($mrpComponentName)))
				{
				print "ERROR: RealTimeBuild: Component name \'$mrp->[1]\' does not match \'$mrpComponentName\' in $mrp->[0]\n";
				}
			foreach my $srcitem (@{$mrpobj->GetSrcItems()})
				{
				if ($srcitem =~ /^[\/\\]/)
					{
					# Remove source prefix
					$srcitem = $self->_StripFile($srcitem);
					}
				else
					{
					# Relative source item
					$srcitem = File::Spec->catdir($path, $srcitem);
					}

				my $rootedmrp = $path.$name;
				if ($self->_RemoveBaseFromPath($srcitem, \$rootedmrp))
					{
					$selfowned = 1;
					}

				$self->AddSrcItem($srcitem, $mrpComponentName);
				}
			if ($self->iCheckCase())
				{
				foreach my $binexpitem (@{$mrpobj->GetBinExpItems()})
					{
					# Check lower case
					if ($binexpitem =~ /[A-Z]/)
						{
						print "REMARK: [$mrpComponentName] Binary/export file $binexpitem should be lower case\n";
						}
					}
				}

			if (!$selfowned)
				{
				print "REMARK: .mrp file '$mrp->[0]' does not include itself as source\n"; 
				}
			}
		}
	return $loaded;
	}
	
# AddSrcItem - Records a source file, usually taken from an .mrp file
#
# Parameters:
# $aItem - the source file name
# $aComponent - the name of the component which claimed the file
#
# Returns: None
# Dies: Not normally; only if the source hash data structure gets corrupted
sub AddSrcItem($$)
	{
	my $self = shift;
	my ($aItem, $aComponent) = @_;

	my $item = $aItem;

	# Worth checking that the file exists
	my $truePath = File::Spec->catdir($self->iSrcRoot(), $item);
	if (($item !~ /^\\component_defs/i) && (!-e $truePath))
		{
		print "ERROR: RealTimeBuild: '$aComponent' owns $item, but that path doesn't exist\n";
		$item = ""; # No point adding this path to the tree	
		}
	else
		{
		# Check case consistency
		$self->_CheckCase($truePath) or print "WARNING: [$aComponent] Case of '".$truePath."' does not match the file system. Should be ".$self->iCorrectedCase()."\n";
		}
	
	$item =~ s/^[\/\\]*//; # Remove preceding slashes

	my @path = split(/[\/\\]+/,$item);

	my $dir = $self->iSrcItems();
	while ((scalar @path) > 0)
		{
		my $subdir = lc(shift @path);
	
		if (scalar(@path) == 0)
			{
			# Just enter the final path segment
			if (exists($dir->{$subdir}))
				{
				# Someone already owns at least part of this path
				if (!ref($dir->{$subdir}))
					{
					# Someone owns the whole of this path
					my $conflict = $dir->{$subdir};

					print "REMARK: $aComponent and $conflict both own $item\n";
					}
				else
					{
					if (ref($dir->{$subdir}) ne "HASH")
						{
						die "ERROR: Source hash is corrupted\n";
						}
					else
						{
						# Someone owns a child of this path
						my $childtree = $dir->{$subdir};

						my @conflicts = $self->_GetTreeComps($childtree);
						print "REMARK: $aComponent owns $item, which is already owned by the following component(s): ".join(", ",@conflicts)."\n";
						}
					}
				}
			$dir->{$subdir} = $aComponent;
			}
		else
			{
			# Need to enter another subdirectory
			
			if (exists($dir->{$subdir}))
				{
				if (ref($dir->{$subdir}))
					{
					# Someone already has - just do a quick integrity check
					
					if (ref($dir->{$subdir}) ne "HASH")
						{
						die "ERROR: Source hash is corrupted\n";
						}
					}
				else
					{
					# The path from this point on is already owned by a component
					my $conflict = $dir->{$subdir};
					
					print "REMARK: $aComponent and $conflict both own $item\n";
					last;
					}
				}
			else
				{
				$dir->{$subdir} = {};
				}
			}

		$dir = $dir->{$subdir};
		}
	}

# DistillSrc - Compare the recorded source lines against the source path. Delete anything which doesn't match.
#
# Parameters:
# $aDummy - A flag - non-zero means don't actually delete
#
# Returns: None
sub DistillSrc($$)
	{
	my $self = shift;
	my ($aDummy) = @_;

	my $tree = $self->iSrcItems();
	my $path = File::Spec->catdir($self->iSrcRoot(), $self->iSrcPath());

	$path=~s/[\/\\]+/\\/; # Remove multiple slashes

	# Pop the srcpath off the front of the tree
	my @path = split(/[\/\\]/,$self->iSrcPath());

	foreach my $dir (@path)
		{
		if ($dir eq ".")
			{
			next;
			}
		elsif (exists($tree->{lc($dir)}))
			{
			$tree = $tree->{lc($dir)};
		
			if (!ref($tree))
				{
				# Some component owns all of the srcpath
				last;
				}
			}
		else
			{
			# No mrp files claimed any of the source
			$tree = undef;
			last;
			}
		}

	# Now recurse into the tree and delete files
	if (defined($tree))
		{
		if (ref($tree))
			{
			$self->_DistillTree($tree, $path, $aDummy);
			}
		else
			{
			print "REMARK: All source owned by component '$tree'; no action\n";
			}
		}
	else
		{
		print "WARNING: No .mrp files claim any source; removing $path\n";
		$self->_DeletePath($path, $aDummy);
		}
	}

# Print - Display the source tree
#
# Parameters:
# $aDepth - The number of levels of the tree to show. 0 = all levels
#
# Returns: None
sub Print($$)
	{
	my $self = shift;

	my ($aDepth) = @_;

	$self->_PrintTree("", $self->iSrcItems(), $aDepth);
	}
	
# *** Private methods ***
# *** 

# _LoadConfig - (private) Reads a configuration file, as used by makecbr
#
# Parameters:
# $aConfig - filename of the configuration file
#
# Returns:
# (files, mrps) - where files and mrps are listrefs containing component lists and
# mrp files respectively
#
sub _LoadConfig($)
	{
	my $self = shift;
	my ($aConfig) = @_;
	
	my @files = ();
	my @mrps = ();
	
	my $config = New CConfig($aConfig);

	if (!defined $config)
		{
		die "Couldn't load config file '$aConfig'\n";
		}
		
	# Extract the interesting items into our lists
	push @mrps, $config->Get("gt+techview baseline mrp location");
	push @mrps, $config->Get("gt only baseline mrp location");
	push @files, $config->Get("techview component list");
	push @files, $config->Get("gt component list");
	
	# Remove any items we couldn't find
	@mrps = grep(defined($_), @mrps);
	@files = grep(defined($_), @files);
	
	return (\@files, \@mrps);
	}

# _StripFile - (private) Remover of src prefix. Also maps product directories
#
# Parameters:
# $aFile - Filename to process
#
# Returns: The processed filename
#
sub _StripFile($)
	{
	my $self = shift;
	my ($aFile) = @_;

	my $file = $aFile;

	# Map the product dirs
	my $platform = $self->iPlatform();
	$file =~ s#^[\/\\]?product[\/\\]#/sf/os/unref/orphan/cedprd/#i;

	# Remove the prefix
	my $prefix = $self->iSrcPrefix();
	
	if (defined $prefix)
		{
		my $mapped = $file; # Keep a copy in case we can't remove the prefix
		
		if (!$self->_RemoveBaseFromPath($prefix, \$file))
			{
			$file = $mapped;
			}
		}
	
	return $file;
	}
	
# _PlantFile - (private) Add src root to file. Also take off src prefix
#
# Parameters:
# $aFile - Filename to process
#
# Returns: The processed filename
#
sub _PlantFile($)
	{
	my $self = shift;
	my ($aFile) = @_;

	my $file = $aFile;

	# Remove the prefix
	$file = $self->_StripFile($file);

	# Plant the file in the src root
	$file = File::Spec->catdir($self->iSrcRoot(), $file);
	
	# Ensure all slashes are normalised to a single backslash
	$file =~ s/[\/\\]+/\\/; 
	
	return $file;
	}

# _RemoveBaseFromPath - (private) Remove a base path from the root of a filename.
#
# Parameters:
# $aBase - The base path to remove
# $$aFile - Filename to process (scalar reference)
#
# Returns: True if the file was under the base path, false otherwise
#   $$aFile may be corrupted if the return is false
sub _RemoveBaseFromPath($)
	{
	my $self = shift;
	my ($aBase, $aFile) = @_;

	my $base = $aBase;
	$base =~ s/^[\/\\]*//; # Remove extra slashes
	$base =~ s/[\/\\]*$//;

	my @base = split(/[\/\\]+/, $base);

	$$aFile =~ s/^[\/\\]*//; # Remove preceding slashes
	
	my $matched = 1;
	my $filedir;
	
	foreach my $dir (@base)
		{
		if ($$aFile =~ /[\/\\]/)
			{
			# Split off the bottom dir
			$$aFile =~ /([^\/\\]*)[\/\\]+(.*)$/;
			($filedir, $$aFile) = ($1, $2, $3);
			}
		else
			{
			# Special case - no more dirs
			$filedir = $$aFile;
			$$aFile = "";
			}
		if (lc($filedir) ne lc($dir))
			{
			# Base doesn't match
			$matched = 0;
			last;
			}
		}
	
	return $matched;
	}

# _CheckCase - (private) Given a literal filename, compares the case of the
#                        file on the filesystem against the filename i.e. it
#                        can be used to enforce case sensitivity
#
# Parameters:
# $aFilename - The literal filename
#
# Returns: True if the file matches the supplied case.
#          True if the file doesn't exist at all (user is expected to check that separately)
#          True if case checking has been disabled.
#          False otherwise (if the file exists but under a differing case).
#
# If false, the correctly cased name is present through $self->iCorrectedCase()
sub _CheckCase($)
{
	my $self = shift;
	my ($aFile) = @_;

	return 1 if !($self->iCheckCase()); # checking disabled
	return 1 if ($^O !~ /win32/i); # only works on Windows anyway
	
	return 1 if (!-e $aFile); # file not found (under case-insensitive checking)
	
	$self->iCorrectedCase(Win32::GetLongPathName($aFile));
	return ($aFile eq $self->iCorrectedCase());
}

# _DistillTree - (private) Given a src tree and a dir, clean out any unowned files
#
# Parameters:
# %$aTree - The source tree (hash ref containing nested hash refs and string leaves)
# $aDir - The directory to compare against
# $aDummy - A flag - non-zero means don't do the actual deletion
#
# Returns: A flag - non-zero if there were any owned files present
sub _DistillTree($$$)
	{
	my $self = shift;
	my ($aTree, $aDir, $aDummy) = @_;


	my $keptsome = 0;

	if (opendir(DIR, $aDir))
	{	
		my $dir = $aDir;
		$dir =~ s/[\/\\]*$//; # Remove trailing / from dir
	
		foreach my $entry (readdir(DIR))
			{
			my $path = $dir."\\".$entry;
	
			if ($entry =~ /^\.\.?$/)
				{
				next;
				}
			elsif (exists $aTree->{lc($entry)})
				{
				my $treeentry = $aTree->{lc($entry)};
				if (ref($treeentry) eq "HASH")
					{
					# Part of this path is owned
					if (-d $path)
						{
						# Recurse into path
						my $keep = $self->_DistillTree($treeentry, $path, $aDummy);
						if ($keep)
							{
							$keptsome = 1;
							}
						else
							{
							# Correction; none of this path was owned
							$self->_DeletePath($path, $aDummy);
							}
						}
					elsif (-f $path)
						{
						my @comps = $self->_GetTreeComps($treeentry);
						print "ERROR: RealTimeBuild: $path is a file, yet is used as a directory in components: ".join(", ",@comps)."\n";
						}
					else
						{
						print "ERROR: $path has disappeared while it was being examined\n";
						}
					}
				elsif (!ref($treeentry))
					{
					# This path is completely owned
					$keptsome = 1;
					next;
					}
				else
					{
					die "ERROR: Source hash is corrupted\n";
					}
				}
			else
				{
				$self->_DeletePath($path, $aDummy);
				}
			}
		
		closedir(DIR);
		}
	else
		{
			warn "ERROR: RealTimeBuild: Couldn't open directory '$aDir' for reading\n";
		}

	return $keptsome;
	}

# _GetTreeComps - (private) Get all the leaves out of a tree (or component
#                           names out of a source tree)
# Parameters:
# %$aTree - The source tree (hash ref containing nested hash refs and string leaves)
# 
# Returns: A list of strings found at the leaves (or component names)
sub _GetTreeComps($)
	{
	my $self = shift;
	my ($aTree) = @_;

	my @comps = ();

	foreach my $entry (keys(%$aTree))
		{
		if (ref($aTree->{$entry}) eq "HASH")
			{
			push @comps, $self->_GetTreeComps($aTree->{$entry});
			}
		elsif (!ref($aTree->{$entry}))
			{
			push @comps, $aTree->{$entry};
			}
		else
			{
			die "ERROR: Source hash is corrupted\n";
			}
		}
		
	return @comps;
	}

# _DeletePath - (private) Safe path deletion (file or dir)
#
# $aPath - The path to delet
# $aDummy  - A flag - non-zero means don't actually delete
#
# Returns: None. Prints warnings if deletion fails. Dies only in exceptional circumstances
sub _DeletePath($$)
	{
	my $self = shift;

	my ($aPath, $aDummy) = @_;

	if (-d $aPath)
		{
		if ($aDummy)
			{
			print "DUMMY: Directory $aPath is not specified in any .mrp file\n";
			}
		else
			{
			print "REMARK: Deleting directory $aPath; ";
			my $files = rmtree($aPath);
			if ($files)
				{
				print "$files items removed\n";
				}
			else
				{
				print "\nWARNING: Problem removing directory $aPath\n";
				}
			}
		}
	elsif (-f $aPath)
		{
		if ($aDummy)
			{
			print "DUMMY: File $aPath is not specified in any .mrp file\n";
			}
		else
			{
				unless($aPath =~ /distribution.policy.s60/i)
				{
					print "REMARK: Deleting file $aPath\n";
					unlink $aPath or print "WARNING: Problem deleting file $aPath\n";
				}
			}
		}
	else
		{
		warn "ERROR: Can't delete path $aPath; not a file or directory\n";
		}
	}

# _PrintTree - Display a subset of the source tree
#
# Parameters:
# $aPrefix - The string to prefix all paths
# $aDepth - The number of levels of the tree to show. 0 = all levels
#
# Returns: None
sub _PrintTree($$$)
        {
	my $self = shift;
	
        my ($aPrefix, $aTree, $aDepth) = @_;

	my $prefix = "";
	
	if ($aPrefix ne "")
		{
		$prefix = $aPrefix."\\";
		}

        foreach my $key (sort(keys(%$aTree)))
                {
                if (ref($aTree->{$key}))
                        {
			if ($aDepth!=1)
				{
				my $newprefix = $prefix.$key;
				
				if ($key eq "")
					{
					$newprefix.="{empty}";
					}

                        	$self->_PrintTree($newprefix, $aTree->{$key}, $aDepth-1);
				}
			else
				{
				print $prefix.$key."\\...\n";
				}
                        }
                else
                        {
                        print $prefix.$key." = ".$aTree->{$key}."\n";
                        }
                }
        }

1;
