srctools/distillsrc/distillsrc.pm
changeset 607 378360dbbdba
parent 602 3145852acc89
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/srctools/distillsrc/distillsrc.pm	Wed Jun 30 11:35:58 2010 +0800
@@ -0,0 +1,898 @@
+#!/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;