File Coverage

blib/lib/Slackware/SBoKeeper/Database.pm
Criterion Covered Total %
statement 248 261 95.0
branch 54 74 72.9
condition 8 14 57.1
subroutine 32 32 100.0
pod 23 25 92.0
total 365 406 89.9


line stmt bran cond sub pod time code
1             package Slackware::SBoKeeper::Database;
2 3     3   144228 use 5.016;
  3         13  
3             our $VERSION = '2.06';
4 3     3   21 use strict;
  3         8  
  3         95  
5 3     3   13 use warnings;
  3         5  
  3         226  
6              
7 3     3   30 use List::Util qw(any uniq);
  3         9  
  3         11344  
8              
9             sub write_data {
10              
11 16     16 0 28 my $data = shift;
12 16         71 my $out = shift;
13              
14 16         37 my $outstr = '';
15              
16 16 50 0     2073 open my $fh, '>', $out // \$outstr
      100        
17             or die sprintf "Failed to open %s for writing: $!\n", $out // 'in-memory scalar';
18              
19 16         60 foreach my $p (sort keys %{$data}) {
  16         141  
20              
21 109         207 say { $fh } "PACKAGE: $p";
  109         358  
22 109         163 say { $fh } "DEPS: ", join(' ', @{$data->{$p}->{Deps}});
  109         192  
  109         291  
23 109         203 say { $fh } "MANUAL: $data->{$p}->{Manual}";
  109         289  
24 109         161 say { $fh } '%%';
  109         228  
25              
26             }
27              
28 16         1553 close $fh;
29              
30 16   66     215 return $out // $outstr;
31              
32             }
33              
34             sub read_data {
35              
36 31     31 0 89 my $file = shift;
37 31   50     100 my $blacklist = shift // {};
38              
39 31         78 my $data = {};
40              
41 31 50       1635 open my $fh, '<', $file or die "Failed to open $file for reading: $!\n";
42              
43 31         108 my $pkg = '';
44 31         118 my $lnum = 1;
45              
46 31         952 while (my $l = readline $fh) {
47              
48 1372         2244 chomp $l;
49              
50 1372 100       4418 if ($l eq '%%') {
    100          
    50          
    100          
    50          
51 343         584 $pkg = '';
52             } elsif ($l =~ /^PACKAGE: /) {
53 343         940 $pkg = $l =~ s/^PACKAGE: //r;
54 343         1074 $data->{$pkg} = {};
55             } elsif ($pkg eq '') {
56 0         0 die "Bad line in $file at line $lnum: PACKAGE not set\n";
57             } elsif ($l =~ /^DEPS: /) {
58 343         930 my $depstr = $l =~ s/^DEPS: //r;
59 343         1254 @{$data->{$pkg}->{Deps}} =
60 343         1118 grep { not exists $blacklist->{$_} }
  345         895  
61             split /\s/, $depstr;
62             } elsif ($l =~ /^MANUAL: /) {
63 343         960 my $manual = $l =~ s/^MANUAL: //r;
64 343 100       962 $data->{$pkg}->{Manual} = $manual eq '1' ? 1 : 0;
65             } else {
66 0         0 die "Bad line in $file at line $lnum\n";
67             }
68              
69 1372         4154 $lnum++;
70              
71             }
72              
73 31         489 close $fh;
74              
75             # Weed out blacklisted packages
76 31         71 for my $p (keys %{$blacklist}) {
  31         127  
77 31         80 delete $data->{$p};
78             }
79              
80 31         255 return $data;
81              
82             }
83              
84             sub new {
85              
86 41     41 1 184207 my $class = shift;
87 41         123 my $file = shift;
88 41         88 my $sbodir = shift;
89 41   100     163 my $blacklist = shift // {};
90              
91 41         123 $blacklist->{'%README%'} = 1;
92              
93 41         202 my $self = {
94             _data => {},
95             _sbodir => '',
96             _blacklist => $blacklist,
97             };
98              
99 41 100       125 if ($file) {
100 31         129 $self->{_data} = read_data($file, $self->{_blacklist});
101             }
102              
103 41         146 $self->{_sbodir} = $sbodir;
104              
105 41         205 bless $self, $class;
106 41         165 return $self;
107              
108             }
109              
110             sub add {
111              
112 76     76 1 319 my $self = shift;
113 76         116 my $pkgs = shift;
114 76         116 my $manual = shift;
115              
116 76         120 my %added;
117 76         150 my $n = 0;
118              
119 76         119 foreach my $p (@{$pkgs}) {
  76         368  
120              
121 86 50       220 next if $self->blacklist($p);
122              
123 86 50       224 unless ($self->exists($p)) {
124 0         0 die "$p does not exist in SlackBuild repo\n";
125             }
126              
127             # pkg already present, do not add. Set manual flag if desired.
128 86 100       365 if (defined $self->{_data}->{$p}) {
129 19 100       51 $self->{_data}->{$p}->{Manual} = $manual if $manual;
130 19         54 next;
131             }
132              
133 67         294 $self->{_data}->{$p}->{Manual} = $manual;
134              
135 67         210 my @deps = $self->real_immediate_dependencies($p);
136 67         399 $self->{_data}->{$p}->{Deps} = \@deps;
137              
138 67         272 my @add = $self->add($self->{_data}->{$p}->{Deps}, 0);
139              
140 67         149 for my $ad (@add) {
141 124 50       637 $added{$ad} = $n++ unless exists $added{$ad};
142             }
143              
144 67         218 $added{$p} = $n++;
145              
146             }
147              
148 76         292 return sort { $added{$a} <=> $added{$b} } keys %added;
  321         890  
149              
150             }
151              
152             sub tack {
153              
154 6     6 1 14 my $self = shift;
155 6         14 my $pkgs = shift;
156 6         11 my $manual = shift;
157              
158 6         12 my @tack;
159              
160 6         13 foreach my $p (@{$pkgs}) {
  6         12  
161              
162 15 50       47 next if $self->blacklist($p);
163              
164 15 50       42 unless ($self->exists($p)) {
165 0         0 die "$p does not exist in SlackBuild repo\n";
166             }
167              
168 15 50 33     69 if (defined $self->{_data}->{$p} and $manual) {
169 0         0 $self->{_data}->{$p}->{Manual} = $manual;
170 0         0 push @tack, $p;
171             } else {
172 15         79 $self->{_data}->{$p} = {
173             Deps => [],
174             Manual => $manual,
175             };
176 15         53 push @tack, $p;
177             }
178              
179             }
180              
181 6         35 return @tack;
182              
183             }
184              
185             sub remove {
186              
187 7     7 1 13 my $self = shift;
188 7         13 my $pkgs = shift;
189              
190 7         14 my @rm;
191              
192 7         11 foreach my $p (@{$pkgs}) {
  7         18  
193              
194 30 50       78 unless (defined $self->{_data}->{$p}) {
195 0         0 warn "$p not present in database, not removing\n";
196 0         0 next;
197             }
198              
199 30         126 delete $self->{_data}->{$p};
200              
201 30         92 push @rm, $p;
202              
203             }
204              
205 7         40 return sort @rm;
206              
207             }
208              
209             sub depadd {
210              
211 3     3 1 9 my $self = shift;
212 3         6 my $pkg = shift;
213 3         6 my $deps = shift;
214              
215 3 50       10 unless ($self->has($pkg)) {
216 0         0 die "$pkg is not present in database\n";
217             }
218              
219 3         7 my @add;
220 3         5 foreach my $d (@{$deps}) {
  3         9  
221              
222 6 50       18 next if $self->blacklist($d);
223              
224 6 50       15 unless ($self->has($d)) {
225 0         0 warn "$d not present in database, skipping\n";
226 0         0 next;
227             }
228              
229 6 50   6   22 unless (any { $d eq $_ } @{$self->{_data}->{$pkg}->{Deps}}) {
  6         15  
  6         29  
230 6         13 push @{$self->{_data}->{$pkg}->{Deps}}, $d;
  6         14  
231 6         24 push @add, $d;
232             }
233              
234             }
235              
236 3         15 return @add;
237              
238             }
239              
240             sub depremove {
241              
242 2     2 1 7 my $self = shift;
243 2         6 my $pkg = shift;
244 2         4 my $deps = shift;
245              
246 2         4 my @kept;
247             my @rm;
248 2         5 foreach my $p (@{$self->{_data}->{$pkg}->{Deps}}) {
  2         25  
249 6 100   9   35 if (any { $p eq $_ } @{$deps}) {
  9         24  
  6         20  
250 4         18 push @rm, $p;
251             } else {
252 2         7 push @kept, $p;
253             }
254             }
255              
256 2         8 $self->{_data}->{$pkg}->{Deps} = \@kept;
257              
258 2         12 return @rm;
259              
260             }
261              
262             sub has {
263              
264 126     126 1 213 my $self = shift;
265 126         201 my $pkg = shift;
266              
267 126         483 return defined $self->{_data}->{$pkg};
268              
269             }
270              
271             sub packages {
272              
273 116     116 1 644 my $self = shift;
274              
275 116         186 return sort keys %{$self->{_data}};
  116         1079  
276              
277             }
278              
279             sub missing {
280              
281 5     5 1 14 my $self = shift;
282              
283 5         13 my %missing;
284              
285 5         18 foreach my $p ($self->packages) {
286              
287             my @pmissing =
288 49         139 grep { !$self->has($_) }
  67         178  
289             $self->real_immediate_dependencies($p);
290              
291 49 100       174 push @{$missing{$p}}, @pmissing if @pmissing;
  5         28  
292              
293             }
294              
295 5         52 return %missing;
296              
297             }
298              
299             sub extradeps {
300              
301 2     2 1 6 my $self = shift;
302              
303 2         6 my @pkgs = $self->packages;
304              
305 2         5 my %extra;
306              
307 2         6 foreach my $p (@pkgs) {
308              
309 15         38 my %realdeps = map { $_ => 1 } $self->real_immediate_dependencies($p);
  16         62  
310              
311             my @pextra =
312 15         65 grep { !defined $realdeps{$_} }
  17         46  
313             $self->immediate_dependencies($p);
314              
315 15 100       59 push @{$extra{$p}}, @pextra if @pextra;
  2         11  
316              
317             }
318              
319 2         17 return %extra;
320              
321             }
322              
323             sub is_necessary {
324              
325 82     82 1 157 my $self = shift;
326 82         156 my $pkg = shift;
327              
328 82 50       236 unless (defined $self->{_data}->{$pkg}) {
329 0         0 return 0;
330             }
331              
332 82 100       217 if ($self->{_data}->{$pkg}->{Manual}) {
333 6         38 return 1;
334             }
335              
336             # Check if $pkg is a dependency of any manually installed package
337              
338             return
339 66     66   186 any { $self->is_dependency($pkg, $_) }
340 76         338 grep { $self->is_manual($_) }
  892         1824  
341             $self->packages
342             ;
343              
344             }
345              
346             sub is_dependency {
347              
348 86     86 1 1059 my $self = shift;
349 86         132 my $dep = shift;
350 86         139 my $of = shift;
351              
352 86         127 foreach my $p (@{$self->{_data}->{$of}->{Deps}}) {
  86         246  
353              
354 20 100       35 if ($p eq $dep) {
355 5         13 return 1;
356             }
357              
358 15 100       25 if ($self->is_dependency($dep, $p)) {
359 2         5 return 1;
360             }
361              
362             }
363              
364 79         607 return 0;
365              
366             }
367              
368             sub is_immediate_dependency {
369              
370 85     85 1 1103 my $self = shift;
371 85         139 my $dep = shift;
372 85         123 my $of = shift;
373              
374 85         131 foreach my $p (@{$self->{_data}->{$of}->{Deps}}) {
  85         196  
375 90 100       212 if ($p eq $dep) {
376 8         29 return 1;
377             }
378             }
379              
380 77         185 return 0;
381              
382             }
383              
384             sub is_manual {
385              
386 975     975 1 1542 my $self = shift;
387 975         1581 my $pkg = shift;
388              
389 975 100       2760 return $self->{_data}->{$pkg}->{Manual} ? 1 : 0;
390              
391             }
392              
393             sub exists {
394              
395 108     108 1 1658 my $self = shift;
396 108         180 my $pkg = shift;
397              
398 108 50       241 return 0 if $self->blacklist($pkg);
399              
400 108 100       11145 if (() = glob "$self->{_sbodir}/*/$pkg/$pkg.info") {
401 107         695 return 1;
402             } else {
403 1         11 return 0;
404             }
405              
406             }
407              
408             sub blacklist {
409              
410 387     387 1 616 my $self = shift;
411 387         736 my $pkg = shift;
412              
413 387         1767 return exists $self->{_blacklist}->{$pkg};
414              
415             }
416              
417             sub dependencies {
418              
419 12     12 1 20 my $self = shift;
420 12         20 my $pkg = shift;
421              
422 12         20 my @deps;
423              
424 12         26 @deps = $self->immediate_dependencies($pkg);
425              
426 12         25 foreach my $d (@deps) {
427 11         26 push @deps, $self->dependencies($d);
428             }
429              
430 12         49 return uniq sort @deps;
431              
432             }
433              
434             sub immediate_dependencies {
435              
436 60     60 1 102 my $self = shift;
437 60         95 my $pkg = shift;
438              
439 60         98 return sort @{$self->{_data}->{$pkg}->{Deps}};
  60         313  
440              
441             }
442              
443             sub real_dependencies {
444              
445 12     12 1 15 my $self = shift;
446 12         14 my $pkg = shift;
447              
448 12         11 my @deps;
449              
450 12         22 @deps = $self->real_immediate_dependencies($pkg);
451              
452 12         23 foreach my $d (@deps) {
453 11         21 push @deps, $self->real_dependencies($d);
454             }
455              
456 12         53 return uniq sort @deps;
457              
458             }
459              
460             sub real_immediate_dependencies {
461              
462 144     144 1 621 my $self = shift;
463 144         245 my $pkg = shift;
464              
465 144         241 my @deps;
466              
467 144         13022 my ($info) = glob "$self->{_sbodir}/*/$pkg/$pkg.info";
468              
469 144 50       621 die "Could not find $pkg in $self->{_sbodir}\n" unless $info;
470              
471 144 50       5567 open my $fh, '<', $info
472             or die "Failed to open $info for reading: $!\n";
473              
474 144         2411 while (my $l = readline $fh) {
475              
476 921         1548 chomp $l;
477              
478 921 100       3574 next unless $l =~ /^REQUIRES=".*("|\\)$/;
479              
480 144         694 my ($depstr) = $l =~ /^REQUIRES="(.*)("|\\)/;
481              
482 144         616 @deps = grep { !$self->blacklist($_) } split /\s/, $depstr;
  168         501  
483              
484 144         589 while (substr($l, -1) eq '\\') {
485              
486 3         8 $l = readline $fh;
487 3         6 chomp $l;
488              
489 3         12 ($depstr) = $l =~ /(^.*)("|\\)/;
490              
491 3         9 push @deps, grep { !$self->blacklist($_) } split(" ", $depstr);
  2         6  
492              
493             }
494              
495 144         300 last;
496              
497             }
498              
499 144         2240 close $fh;
500              
501 144         1047 return sort @deps;
502              
503             }
504              
505             sub reverse_dependencies {
506              
507 7     7 1 17 my $self = shift;
508 7         15 my $pkg = shift;
509              
510             return grep {
511 7         21 $self->is_immediate_dependency($pkg, $_)
  80         184  
512             } $self->packages;
513              
514             }
515              
516             sub unmanual {
517              
518 5     5 1 11 my $self = shift;
519 5         9 my $pkg = shift;
520              
521 5 50       16 unless (defined $self->{_data}->{$pkg}) {
522 0         0 return 0;
523             }
524              
525 5         10 $self->{_data}->{$pkg}->{Manual} = 0;
526              
527 5         10 return 1;
528              
529             }
530              
531             sub write {
532              
533 16     16 1 41 my $self = shift;
534 16         34 my $path = shift;
535              
536 16         102 write_data($self->{_data}, $path);
537              
538             }
539              
540             1;
541              
542              
543              
544             =head1 NAME
545              
546             Slackware::SBoKeeper::Database - Read/write sbokeeper databases
547              
548             =head1 SYNOPSIS
549              
550             use Slackware::SBoKeeper::Database;
551              
552             my $database = Slackware::SBoKeeper::Database->new($file, $repo);
553             ...
554              
555             =head1 DESCRIPTION
556              
557             Slackware::SBoKeeper::Database is a module that handles reading and writing
558             L package database files. It is not meant to be used outside of
559             L. For user documentation of L, please consult its manual.
560              
561             =head1 SUBROUTINES/METHODS
562              
563             =head2 new($path, $sbodir, [ $blacklist ])
564              
565             Returns blessed Slackware::SBoKeeper::Database object. $path is the path to a
566             file containing B data. If $path is '', creates an empty
567             database. $sbodir is the directory where the SBo repository is stored.
568              
569             $blacklist is a hash ref of packages to ignore. Defaults to empty hash ref if
570             no hash is supplied.
571              
572             =head2 add($pkgs, $manual)
573              
574             Add array ref of pkgs and their dependencies to object. If $manual is true,
575             $pkgs are set to manually added (dependencies are still not).
576              
577             Returns array of packages added.
578              
579             =head2 tack($pkgs, $manual)
580              
581             Add array ref of pkgs to database. $manual determines whether they are marked
582             as manually added or not. Does not pull in dependencies.
583              
584             Returns array of packages added.
585              
586             =head2 remove($pkgs)
587              
588             Remove array ref pkgs from object. Dependencies pulled in from removed packages
589             will still remain.
590              
591             Returns array of packages removed.
592              
593             =head2 depadd($pkg, $deps)
594              
595             Add array ref $deps to $pkg's dependencies. $deps must be a list of packages
596             already present in the database.
597              
598             Returns list of dependencies added to $pkg.
599              
600             =head2 depremove($pkg, $deps)
601              
602             Removes array ref $deps from $pkg's dependency list.
603              
604             Returns list of dependencies removed.
605              
606             =head2 has($pkg)
607              
608             Returns 1 or 0 depending on whether $pkg is currently in the database.
609              
610             =head2 packages()
611              
612             Returns array of packages present in database.
613              
614             =head2 missing()
615              
616             Returns hash of packages and their missing dependencies, according to the
617             SlackBuild repo.
618              
619             =head2 extradeps()
620              
621             Returns hash of packages with extra dependencies. An extra dependency is a
622             dependency that is not required by the package in the SlackBuild repo.
623              
624             =head2 is_necessary($pkg)
625              
626             Checks to see if $pkg is necessary (manually added or dependency on a manually
627             added package). Returns 1 for yes, 0 for no.
628              
629             =head2 is_dependency($dep, $of)
630              
631             Checks to see if $dep is a dependency (or a dependency of a dependency, etc.) of
632             $of. Returns 1 for yes, 0 for no.
633              
634             $dep and $of must already be added to the object.
635              
636             =head2 is_immediate_dependency($dep, $of)
637              
638             Checks to see if $dep is a dependency of $of. Returns 1 for yes, 0 for no.
639              
640             $dep and $of must already be added to the object.
641              
642             =head2 is_manual($pkg)
643              
644             Checks if $pkg is manually installed. Returns 1 for yes, 0 no.
645              
646             =head2 exists($pkg)
647              
648             Checks if $pkg is present in repo. Returns 1 for yes, 0 for no.
649              
650             =head2 blacklist($pkg)
651              
652             Checks if $pkg is in the blacklist. Returns 1 for yes, 0 for no.
653              
654             =head2 dependencies($pkg)
655              
656             Returns list of packages that are a dependency of $pkg, according to the
657             database.
658              
659             =head2 immediate_dependencies($pkg)
660              
661             Returns list of packages that are an immediate dependency of $pkg, according to
662             the database. Does not return dependencies of those dependencies.
663              
664             =head2 real_dependencies($pkg)
665              
666             Returns list of packages that are a dependency of $pkg, according to the
667             SlackBuild repo. $pkg does not have to have been added previously.
668              
669             =head2 real_immediate_dependencies($pkg)
670              
671             Returns list of packages that are an immediate dependency of $pkg, according
672             to the SlackBuild repo. Does not return packages that are dependencies of those
673             dependencies. $pkg does not have to have been added previously.
674              
675             =head2 reverse_dependencies($pkg)
676              
677             Returns list of packages that depend on $pkg, according to the sbokeeper
678             database.
679              
680             =head2 unmanual($pkg)
681              
682             Unset $pkg as manually installed. Returns 1 if successful, 0 if not.
683              
684             =head2 write([$path])
685              
686             Writes data file to C<$path>, or returns string of would-be written data if
687             C<$path> is omitted.
688              
689             =head1 DATA FILES
690              
691             B data files are text files. Data files contain packages, which are
692             a series of lines that contain package information ended by a pair of percentage
693             signs (%%). A package entry should look something like this:
694              
695             PACKAGE: libreoffice
696             DEPS: avahi zulu-openjdk8
697             MANUAL: 1
698             %%
699              
700             =over 4
701              
702             =item PACKAGE
703              
704             Name of the package, which must have a corresponding SlackBuild in the
705             configured SlackBuild repo. This must be the first line in a package entry.
706              
707             =item DEPS
708              
709             Whitespace-seperated list of packages that PACKAGE depends on. Each package
710             must be present in the SlackBuild repo.
711              
712             =item MANUAL
713              
714             Specifies whether the package was manually added or not. 1 for yes, 0 for no.
715              
716             =item %%
717              
718             Marks the end of the current package entry.
719              
720             =back
721              
722             =head1 AUTHOR
723              
724             Written by Samuel Young Esamyoung12788@gmail.comE.
725              
726             =head1 BUGS
727              
728             This module does not know how to handle circular dependencies. This should not
729             be a problem if you stick with the official SlackBuild repo. One should
730             exercise caution when using the depadd method, as it can easily introduce
731             circular dependencies.
732              
733             Report bugs on my Codeberg, L.
734              
735             =head1 COPYRIGHT
736              
737             Copyright (C) 2024-2025 Samuel Young
738              
739             This program is free software; you can redistribute it and/or modify it under
740             the terms of either: the GNU General Public License as published by the Free
741             Software Foundation; or the Artistic License.
742              
743             =head1 SEE ALSO
744              
745             L
746              
747             =cut