# SPDX-FileCopyrightText: 2013, 2014, 2017, 2023 Michael Pyne # # SPDX-License-Identifier: GPL-2.0-or-later package ksb::KDEProjectsReader 0.50; # Class: KDEProjectsReader # # Enumerates and provides basic metadata of KDE projects, based on # the YAML metadata included in sysadmin/repo-management. use ksb; use File::Find; use List::Util qw(uniq); use ksb::BuildException; use ksb::Debug; sub _verifyYAMLModuleLoaded { # Load YAML-reading module if available without causing compile error if it # isn't. Note that YAML::Tiny and YAML do not work since some metadata files # use features it doesn't support my @YAML_Opts = qw(Dump Load LoadFile); my @YAML_Mods = qw(YAML::XS YAML::PP); # Do not use YAML::Syck here, because it incorrectly interprets false as true. See https://invent.kde.org/sdk/kdesrc-build/-/issues/133 my $success = 0; foreach my $mod (@YAML_Mods) { $success ||= eval "require $mod; $mod->import(\@YAML_Opts); 1;"; last if $success; } if (!$success) { die "Unable to load one of " . join(', ', @YAML_Mods) . " modules, one of which is needed to handle KDE project data."; } } # Method: new # # Constructs a new KDEProjectsReader. This doesn't contradict any part of the class # documentation which claims this class is a singleton however. This should be # called as a method (e.g. KDEProjectsReader->new(...)). # # Parameters: # $projectMetadataModule - ksb::Module reference to the repo-metadata module. sub new { my $class = shift; my $projectMetadataModule = shift; _verifyYAMLModuleLoaded(); my $self = { # Maps short names to repo info blocks repositories => { } }; $self = bless ($self, $class); $self->_readProjectData($projectMetadataModule); return $self; } # The 'main' method for this class. Reads in *all* KDE projects and notes # their details for later queries. # Be careful, can throw exceptions. sub _readProjectData ($self, $projectMetadataModule) { if (isTesting()) { $self->_loadMockProjectData(); return; } my $srcdir = $projectMetadataModule->fullpath('source'); croak_runtime("No such source directory $srcdir!") unless -d $srcdir; # NOTE: This is approx 1280 entries as of Feb 2023. Need to memoize this # so that only entries that are used end up being read. # The obvious thing of using path info to guess module name doesn't work # (e.g. maui-booth has a disk path of maui/booth in repo-metadata, not maui/maui-booth) my $repoMetaFiles = Mojo::File->new("$srcdir/projects") ->realpath # resolve /projects symlink first ->list_tree # then recurse through dir tree ->grep(sub ($file) { $file->basename eq 'metadata.yaml' }) ->each(sub ($metadataPath, $) { $self->_readYAML("$metadataPath") }) ; croak_runtime("Failed to find KDE project entries from $srcdir!") unless $repoMetaFiles->size > 0; } # Load some sample projects for use in test mode # Should stay in sync with the data generated by _readYAML sub _loadMockProjectData ($self) { my @projects = qw(kdesrc-build juk kcalc konsole dolphin); for my $project (@projects) { my $repoData = { fullName => "test/$project", repo => "kde:$project.git", name => $project, active => 1, found_by => 'direct', }; $self->{repositories}->{$project} = $repoData; } } sub _readYAML ($self, $filename) { my $proj_data = LoadFile($filename); # This is already 'covered' as a special metadata module, ignore return if $proj_data->{projectpath} eq 'repo-management'; my $repoPath = $proj_data->{repopath}; my $repoName = $proj_data->{identifier} // $repoPath; # Keep in sync with _loadMockProjectData my $curRepository = { 'fullName' => $proj_data->{projectpath}, 'inventName' => $repoPath, 'repo' => "kde:$repoPath.git", 'name' => $repoName, 'active' => !!$proj_data->{repoactive}, 'found_by' => 'direct', # can be changed in getModulesForProject }; # Find everything after last / my ($inventSuffix) = ($proj_data->{repopath} =~ m,([^/]+$),); my ($legacySuffix) = ($proj_data->{projectpath} =~ m,([^/]+$),); # We can print a message later for modules where the name will change if # the module is actually used $curRepository->{nameChangingTo} = $inventSuffix if $inventSuffix ne $legacySuffix; $self->{repositories}->{$repoName} = $curRepository; } # Note on $proj: A /-separated path is fine, in which case we look # for the right-most part of the full path which matches all of searchProject. # e.g. kde/kdebase/kde-runtime would be matched by a proj of either # "kdebase/kde-runtime" or simply "kde-runtime". sub getModulesForProject { my ($self, $proj) = @_; my $repositoryRef = $self->{repositories}; my @results; my $findResults = sub { my @matchList = grep { _projectPathMatchesWildcardSearch( $repositoryRef->{$_}->{'fullName'}, $proj) } (sort keys %{$repositoryRef}); if ($proj =~ m/\*/) { $repositoryRef->{$_}->{found_by} = 'wildcard' foreach @matchList; } push @results, @matchList; }; # Wildcard matches happen as specified if asked for. # Non-wildcard matches have an implicit "$proj/*" search as well for # compatibility with previous use-modules # Project specifiers ending in .git are forced to be non-wildcarded. if ($proj !~ /\*/ && $proj !~ /\.git$/) { # We have to do a search to account for over-specified module names # like phonon/phonon $findResults->(); # Now setup for a wildcard search to find things like kde/kdelibs/baloo # if just 'kdelibs' is asked for. $proj .= '/*'; } $proj =~ s/\.git$//; # If still no wildcard and no '/' then we can use direct lookup by module # name. if ($proj !~ /\*/ && $proj !~ /\// && exists $repositoryRef->{$proj}) { push @results, $proj; } else { $findResults->(); } # As we run $findResults twice (for example, when proj is "workspace"), remove duplicates @results = uniq(@results); return @{$repositoryRef}{@results}; } # Utility subroutine, returns true if the given kde-project full path (e.g. # kde/kdelibs/nepomuk-core) matches the given search item. # # The search item itself is based on path-components. Each path component in # the search item must be present in the equivalent path component in the # module's project path for a match. A '*' in a path component position for the # search item matches any project path component. # # Finally, the search is pinned to search for a common suffix. E.g. a search # item of 'kdelibs' would match a project path of 'kde/kdelibs' but not # 'kde/kdelibs/nepomuk-core'. However 'kdelibs/*' would match # 'kde/kdelibs/nepomuk-core'. # # First parameter is the full project path from the kde-projects database. # Second parameter is the search item. # Returns true if they match, false otherwise. sub _projectPathMatchesWildcardSearch { my ($projectPath, $searchItem) = @_; my @searchParts = split(m{/}, $searchItem); my @nameStack = split(m{/}, $projectPath); if (scalar @nameStack >= scalar @searchParts) { my $sizeDifference = scalar @nameStack - scalar @searchParts; # We might have to loop if we somehow find the wrong start point for our search. # E.g. looking for a/b/* against a/a/b/c, we'd need to start with the second a. my $i = 0; while ($i <= $sizeDifference) { # Find our common prefix, then ensure the remainder matches item-for-item. for (; $i <= $sizeDifference; $i++) { last if $nameStack[$i] eq $searchParts[0]; } return if $i > $sizeDifference; # Not enough room to find it now # At this point we have synched up nameStack to searchParts, ensure they # match item-for-item. my $found = 1; for (my $j = 0; $found && ($j < @searchParts); $j++) { return 1 if $searchParts[$j] eq '*'; # This always works $found = 0 if $searchParts[$j] ne $nameStack[$i + $j]; } return 1 if $found; # We matched every item to the substring we found. $i++; # Try again } } return; } 1;