You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
438 lines
15 KiB
438 lines
15 KiB
package ksb::DependencyResolver; |
|
|
|
# Class: DependencyResolver |
|
# |
|
# This module handles resolving dependencies between modules. Each "module" |
|
# from the perspective of this resolver is simply a module full name, as |
|
# given by the KDE Project database. (e.g. extragear/utils/kdesrc-build) |
|
|
|
use strict; |
|
use warnings; |
|
use v5.10; |
|
|
|
our $VERSION = '0.20'; |
|
|
|
use ksb::Debug; |
|
use ksb::Util; |
|
use List::Util qw(first); |
|
|
|
# Constructor: new |
|
# |
|
# Constructs a new <DependencyResolver>. No parameters are taken. |
|
# |
|
# Synposis: |
|
# |
|
# > my $resolver = new DependencyResolver; |
|
# > $resolver->readDependencyData(open my $fh, '<', 'file.txt'); |
|
# > $resolver->resolveDependencies(@modules); |
|
sub new |
|
{ |
|
my $class = shift; |
|
|
|
my $self = { |
|
# hash table mapping full module names (m) to a hashref key by branch |
|
# name, the value of which is yet another hashref (see readDependencyData) |
|
dependenciesOf => { }, |
|
|
|
# hash table mapping a wildcarded module name with no branch to a |
|
# listref of module:branch dependencies. |
|
catchAllDependencies => { }, |
|
}; |
|
|
|
return bless $self, $class; |
|
} |
|
|
|
# Method: readDependencyData |
|
# |
|
# Reads in dependency data in a psuedo-Makefile format. |
|
# See kde-build-metadata/dependency-data. |
|
# |
|
# Parameters: |
|
# $self - The DependencyResolver object. |
|
# $fh - Filehandle to read dependencies from (should already be open). |
|
# |
|
# Exceptions: |
|
# Can throw an exception on I/O errors or malformed dependencies. |
|
sub readDependencyData |
|
{ |
|
my $self = assert_isa(shift, 'ksb::DependencyResolver'); |
|
my $fh = shift; |
|
|
|
my $dependenciesOfRef = $self->{dependenciesOf}; |
|
my $dependencyAtom = |
|
qr/ |
|
^\s* # Clear leading whitespace |
|
([^\[:\s]+) # (1) Capture anything not a [, :, or whitespace (dependent item) |
|
\s* # Clear whitespace we didn't capture |
|
(?:\[ # Open a non-capture group... |
|
([^\]:\s]+) # (2) Capture branch name without brackets |
|
])?+ # Close group, make optional, no backtracking |
|
\s* # Clear whitespace we didn't capture |
|
: |
|
\s* |
|
([^\s\[]+) # (3) Capture all non-whitespace (source item) |
|
(?:\s*\[ # Open a non-capture group... |
|
([^\]\s]+) # (4) Capture branch name without brackets |
|
])?+ # Close group, make optional, no backtracking |
|
\s*$ # Ensure no trailing cruft. Any whitespace should end line |
|
/x; # /x Enables extended whitespace mode |
|
|
|
while(my $line = <$fh>) { |
|
# Strip comments, skip empty lines. |
|
$line =~ s{#.*$}{}; |
|
next if $line =~ /^\s*$/; |
|
|
|
if ($line !~ $dependencyAtom) { |
|
croak_internal("Invalid line $line when reading dependency data."); |
|
} |
|
|
|
my ($dependentItem, $dependentBranch, |
|
$sourceItem, $sourceBranch) = $line =~ $dependencyAtom; |
|
|
|
# Ignore "catch-all" dependencies where the source is the catch-all |
|
if ($sourceItem =~ m,\*$,) { |
|
warning ("\tIgnoring dependency on wildcard module grouping " . |
|
"on line $. of kde-build-metadata/dependency-data"); |
|
next; |
|
} |
|
|
|
# Ignore deps on Qt, since we allow system Qt. |
|
next if $sourceItem =~ /^\s*Qt/ || $dependentItem =~ /^\s*Qt/; |
|
|
|
$dependentBranch ||= '*'; # If no branch, apply catch-all flag |
|
$sourceBranch ||= '*'; |
|
|
|
# Handle catch-all dependent groupings |
|
if ($dependentItem =~ /\*$/) { |
|
$self->{catchAllDependencies}->{$dependentItem} //= [ ]; |
|
push @{$self->{catchAllDependencies}->{$dependentItem}}, "$sourceItem:$sourceBranch"; |
|
next; |
|
} |
|
|
|
# Initialize with hashref if not already defined. The hashref will hold |
|
# - => [ ] (list of explicit *NON* dependencies of item:$branch), |
|
# + => [ ] (list of dependencies of item:$branch) |
|
# |
|
# Each dependency item is tracked at the module:branch level, and there |
|
# is always at least an entry for module:*, where '*' means branch |
|
# is unspecified and should only be used to add dependencies, never |
|
# take them away. |
|
# |
|
# Finally, all (non-)dependencies in a list are also of the form |
|
# fullname:branch, where "*" is a valid branch. |
|
$dependenciesOfRef->{"$dependentItem:*"} //= { |
|
'-' => [ ], |
|
'+' => [ ], |
|
}; |
|
|
|
# Create actual branch entry if not present |
|
$dependenciesOfRef->{"$dependentItem:$dependentBranch"} //= { |
|
'-' => [ ], |
|
'+' => [ ], |
|
}; |
|
|
|
my $depKey = (index($sourceItem, '-') == 0) ? '-' : '+'; |
|
$sourceItem =~ s/^-//; |
|
|
|
push @{$dependenciesOfRef->{"$dependentItem:$dependentBranch"}->{$depKey}}, |
|
"$sourceItem:$sourceBranch"; |
|
} |
|
} |
|
|
|
# Function: addInherentDependencies |
|
# |
|
# Internal: |
|
# |
|
# This method adds any full module names as dependencies of any module that |
|
# begins with that full module name. E.g. kde/kdelibs/foo automatically depends |
|
# on kde/kdelibs if both are present in the build. |
|
# |
|
# This is a static function, not an object method. |
|
# |
|
# Parameters: |
|
# |
|
# options - Hashref to the internal options as given to <visitModuleAndDependencies> |
|
# |
|
# Returns: |
|
# |
|
# Nothing. |
|
sub _addInherentDependencies |
|
{ |
|
my $optionsRef = shift; |
|
my $dependenciesOfRef = $optionsRef->{dependenciesOf}; |
|
my $modulesFromNameRef = $optionsRef->{modulesFromName}; |
|
|
|
# It's not good enough to just sort modules and compare one to its |
|
# successor. Consider kde/foo, kde/foobar, kde/foo/a. The dependency |
|
# here would be missed that way. Instead we strip off the last path |
|
# component and see if that matches an existing module name. |
|
for my $testModule (keys %{$modulesFromNameRef}) { |
|
my $candidateBaseModule = $testModule; |
|
|
|
# Remove trailing component, bail if unable to do so. |
|
next unless $candidateBaseModule =~ s(/[^/]+$)(); |
|
|
|
if ($candidateBaseModule && |
|
exists $modulesFromNameRef->{$candidateBaseModule}) |
|
{ |
|
# Add candidateBaseModule as dependency of testModule. |
|
$dependenciesOfRef->{"$testModule:*"} //= { |
|
'-' => [ ], |
|
'+' => [ ], |
|
}; |
|
|
|
my $moduleDepsRef = $dependenciesOfRef->{"$testModule:*"}->{'+'}; |
|
if (!first { $_ eq $candidateBaseModule } @{$moduleDepsRef}) { |
|
debug ("dep-resolv: Adding $testModule as dependency of $candidateBaseModule"); |
|
push @{$moduleDepsRef}, "$candidateBaseModule:*"; |
|
} |
|
} |
|
} |
|
} |
|
|
|
# Function: directDependenciesOf |
|
# |
|
# Internal: |
|
# |
|
# Finds and returns the direct dependencies of the given module at a given |
|
# branch. This requires forming a list of dependencies for the module from the |
|
# "branch neutral" dependencies, adding branch-specific dependencies, and then |
|
# removing any explicit non-dependencies for the given branch, which is why |
|
# this is a separate routine. |
|
# |
|
# Parameters: |
|
# dependenciesOfRef - hashref to the table of dependencies as read by |
|
# <readDependencyData>. |
|
# module - The full name (just the name) of the kde-project module to list |
|
# dependencies of. |
|
# branch - The branch to assume for module. This must be specified, but use |
|
# '*' if you have no specific branch in mind. |
|
# |
|
# Returns: |
|
# A list of dependencies. Every item of the list will be of the form |
|
# "$moduleName:$branch", where $moduleName will be the full kde-project module |
|
# name (e.g. kde/kdelibs) and $branch will be a specific git branch or '*'. |
|
# The order of the entries within the list is not important. |
|
sub _directDependenciesOf |
|
{ |
|
my ($dependenciesOfRef, $module, $branch) = @_; |
|
|
|
my $moduleDepEntryRef = $dependenciesOfRef->{"$module:*"}; |
|
my @directDeps; |
|
my @exclusions; |
|
|
|
return unless $moduleDepEntryRef; |
|
|
|
push @directDeps, @{$moduleDepEntryRef->{'+'}}; |
|
|
|
$moduleDepEntryRef = $dependenciesOfRef->{"$module:$branch"}; |
|
if ($moduleDepEntryRef) { |
|
push @directDeps, @{$moduleDepEntryRef->{'+'}}; |
|
push @exclusions, @{$moduleDepEntryRef->{'-'}}; |
|
} |
|
|
|
foreach my $exclusion (@exclusions) { |
|
my ($moduleName, $branchName) = $exclusion =~ m,^([^:]+):(.*)$,; |
|
|
|
# Remove only modules at the exact given branch as a dep. |
|
# We do this even for "catch-alls", so that specific branches are |
|
# not removed by a "catch-all" exclusion. |
|
@directDeps = grep { $_ ne $exclusion } (@directDeps); |
|
} |
|
|
|
return @directDeps; |
|
} |
|
|
|
# Function: makeCatchAllRules |
|
# |
|
# Internal: |
|
# |
|
# Given the internal dependency options data and a kde-project item, extracts |
|
# all "catch-all" rules that apply to the given item and converts them to |
|
# standard dependencies for that item. The dependency options are then |
|
# appropriately updated. |
|
# |
|
# No checks are done for logical errors (e.g. having the item depend on itself) |
|
# and no provision is made to avoid updating a module that has already had its |
|
# catch-all rules generated. |
|
# |
|
# Parameters: |
|
# optionsRef - The hashref as provided to <_visitModuleAndDependencies> |
|
# item - The kde-project module to generate dependencies of. |
|
sub _makeCatchAllRules |
|
{ |
|
my ($optionsRef, $item) = @_; |
|
my $dependenciesOfRef = $optionsRef->{dependenciesOf}; |
|
|
|
while (my ($catchAll, $deps) = each %{$optionsRef->{catchAllDependencies}}) { |
|
my $prefix = $catchAll; |
|
$prefix =~ s/\*$//; |
|
|
|
if (($item =~ /^$prefix/) || !$prefix) { |
|
my $depEntry = "$item:*"; |
|
$dependenciesOfRef->{$depEntry} //= { |
|
'-' => [ ], |
|
'+' => [ ], |
|
}; |
|
|
|
push @{$dependenciesOfRef->{$depEntry}->{'+'}}, @{$deps}; |
|
} |
|
} |
|
} |
|
|
|
# Function: getBranchOf |
|
# |
|
# Internal: |
|
# |
|
# This function extracts the branch of the given Module by calling its |
|
# scm object's branch-determining method. It also ensures that the branch |
|
# returned was really intended to be a branch (as opposed to a detached HEAD); |
|
# undef is returned when the desired commit is not a branch name, otherwise |
|
# the user-requested branch name is returned. |
|
sub _getBranchOf |
|
{ |
|
my $module = shift; |
|
my ($branch, $type) = $module->scm()->_determinePreferredCheckoutSource($module); |
|
|
|
return ($type eq 'branch' ? $branch : undef); |
|
} |
|
|
|
# Function: visitModuleAndDependencies |
|
# |
|
# Internal: |
|
# |
|
# This method is used to topographically sort dependency data. It accepts a |
|
# <ksb::Module>, ensures that any KDE Projects it depends on present on the |
|
# build list are re-ordered before the module, and then adds the <ksb::Module> |
|
# to the build list (whether it is a KDE Project or not, to preserve ordering). |
|
# |
|
# Parameters: |
|
# optionsRef - hashref to the module dependencies, catch-all dependencies, |
|
# module build list, module name to <ksb::Module> mapping, and auxiliary data |
|
# to see if a module has already been visited. |
|
# module - The <ksb::Module> to properly order in the build list. |
|
# |
|
# Returns: |
|
# Nothing. The proper build order can be read out from the optionsRef passed |
|
# in. |
|
sub _visitModuleAndDependencies |
|
{ |
|
my ($optionsRef, $module) = @_; |
|
assert_isa($module, 'ksb::Module'); |
|
|
|
my $visitedItemsRef = $optionsRef->{visitedItems}; |
|
my $properBuildOrderRef = $optionsRef->{properBuildOrder}; |
|
my $dependenciesOfRef = $optionsRef->{dependenciesOf}; |
|
my $modulesFromNameRef = $optionsRef->{modulesFromName}; |
|
|
|
my $item = ($module->scmType eq 'proj' && $module->fullProjectPath()); |
|
|
|
if (!$item) { |
|
push @{$properBuildOrderRef}, $module; |
|
return; |
|
} |
|
|
|
debug ("dep-resolv: Visiting $item"); |
|
|
|
$visitedItemsRef->{$item} //= 0; |
|
|
|
# This module may have already been added to build. |
|
return if $visitedItemsRef->{$item} == 1; |
|
|
|
# But if the value is 2 that means we've detected a cycle. |
|
if ($visitedItemsRef->{$item} > 1) { |
|
croak_internal("Somehow there is a dependency cycle involving $item! :("); |
|
} |
|
|
|
$visitedItemsRef->{$item} = 2; # Mark as currently-visiting for cycle detection. |
|
|
|
_makeCatchAllRules($optionsRef, $item); |
|
|
|
# We still run dependency analysis even if the user has requested a |
|
# specific tag, as it is possible that there are catch-all dependencies to |
|
# handle. So turn an undefined branch into a set name. |
|
my $branch = _getBranchOf($module) // '?'; |
|
|
|
for my $subItem (_directDependenciesOf($dependenciesOfRef, $item, $branch)) { |
|
my ($subItemName, $subItemBranch) = ($subItem =~ m/^([^:]+):(.*)$/); |
|
croak_internal("Invalid dependency item: $subItem") if !$subItemName; |
|
|
|
next if $subItemName eq $item; # Catch-all deps might make this happen |
|
|
|
debug ("\tdep-resolv: $item:$branch depends on $subItem"); |
|
|
|
my $subModule = $modulesFromNameRef->{$subItemName}; |
|
if (!$subModule) { |
|
whisper (" y[b[*] $module:$branch depends on $subItem, but no module builds $subItem for this run."); |
|
next; |
|
} |
|
|
|
if ($subItemBranch ne '*' && (_getBranchOf($subModule) // '') ne $subItemBranch) { |
|
my $wrongBranch = _getBranchOf($subModule); |
|
error (" r[b[*] $module:$branch needs $subItem, not $subItemName:$wrongBranch"); |
|
} |
|
|
|
_visitModuleAndDependencies($optionsRef, $subModule); |
|
} |
|
|
|
$visitedItemsRef->{$item} = 1; # Mark as done visiting. |
|
push @{$properBuildOrderRef}, $module; |
|
return; |
|
} |
|
|
|
# Function: resolveDependencies |
|
# |
|
# This method takes a list of Modules (real <ksb::Module> objects, not just |
|
# module names). |
|
# |
|
# These modules have their dependencies resolved, and a new list of <Modules> |
|
# is returned, containing the proper build order for the module given. |
|
# |
|
# Only "KDE Project" modules can be re-ordered or otherwise affect the |
|
# build so this currently won't affect Subversion modules or "plain Git" |
|
# modules. |
|
# |
|
# The dependency data must have been read in first (<readDependencyData>). |
|
# |
|
# Parameters: |
|
# |
|
# $self - The DependencyResolver object. |
|
# @modules - List of <Modules> to evaluate, in suggested build order. |
|
# |
|
# Returns: |
|
# |
|
# Modules to build, with the included KDE Project modules in a valid ordering |
|
# based on the currently-read dependency data. KDE Project modules are only |
|
# re-ordered amongst themselves, other module types retain their relative |
|
# positions. |
|
sub resolveDependencies |
|
{ |
|
my $self = assert_isa(shift, 'ksb::DependencyResolver'); |
|
my @modules = @_; |
|
|
|
my $optionsRef = { |
|
visitedItems => { }, |
|
properBuildOrder => [ ], |
|
dependenciesOf => $self->{dependenciesOf}, |
|
catchAllDependencies => $self->{catchAllDependencies}, |
|
|
|
# will map names back to their Modules |
|
modulesFromName => { |
|
map { $_->fullProjectPath() => $_ } |
|
grep { $_->scmType() eq 'proj' } |
|
@modules |
|
}, |
|
}; |
|
|
|
# Adds things like kde/kdelibs/foo to automatically depend on |
|
# kde/kdelibs if both are present in the build. |
|
_addInherentDependencies($optionsRef); |
|
|
|
for my $module (@modules) { |
|
_visitModuleAndDependencies($optionsRef, $module); |
|
} |
|
|
|
return @{$optionsRef->{properBuildOrder}}; |
|
} |
|
|
|
1;
|
|
|