File Coverage

blib/lib/LedgerSMB/Installer.pm
Criterion Covered Total %
statement 56 489 11.4
branch 0 150 0.0
condition 0 42 0.0
subroutine 19 42 45.2
pod 0 5 0.0
total 75 728 10.3


line stmt bran cond sub pod time code
1             package LedgerSMB::Installer v0.999.11;
2              
3 1     1   241048 use v5.20;
  1         3  
4 1     1   457 use experimental qw(signatures);
  1         3163  
  1         6  
5              
6 1     1   148 use Carp qw( croak );
  1         1  
  1         52  
7 1     1   434 use CPAN::Meta::Requirements;
  1         6671  
  1         45  
8 1     1   1092 use English;
  1         3785  
  1         8  
9 1     1   649 use File::Path qw( make_path remove_tree );
  1         2  
  1         98  
10 1     1   7 use File::Spec;
  1         6  
  1         31  
11 1     1   1047 use File::Temp qw( tempfile );
  1         25580  
  1         117  
12 1     1   894 use Getopt::Long qw(GetOptionsFromArray);
  1         16458  
  1         5  
13 1     1   1221 use HTTP::Tiny;
  1         55806  
  1         66  
14 1     1   11 use IO::Handle;
  1         2  
  1         48  
15 1     1   894 use JSON::PP;
  1         20110  
  1         127  
16 1     1   13 use List::Util qw(uniq);
  1         2  
  1         81  
17 1     1   4644 use Module::CoreList;
  1         255576  
  1         341  
18 1     1   74 use version;
  1         11  
  1         8  
19              
20 1     1   892 use Log::Any qw($log);
  1         11274  
  1         29  
21 1     1   3722 use Log::Any::Adapter;
  1         455  
  1         5  
22 1     1   616 use Module::CPANfile;
  1         15479  
  1         56  
23              
24 1     1   1646 use LedgerSMB::Installer::Configuration;
  1         4  
  1         9056  
25              
26             my $INSTALLER_VERSION = 'version CLONED'; # not intended to be the module version
27              
28             my $http = HTTP::Tiny->new( agent => 'LedgerSMB-Installer/0.1' );
29             my $json = JSON::PP->new->canonical;
30              
31 0     0     sub _post_boot_configure($class, $dss, $config) {
  0            
  0            
  0            
  0            
32 0           Log::Any::Adapter->set('Stdout', log_level => $config->loglevel);
33             }
34              
35 0     0     sub _boot($class, $args, $options) {
  0            
  0            
  0            
  0            
36 0           my $dss = $class->_load_dist_support;
37 0           my $config = LedgerSMB::Installer::Configuration->new(
38             sys_pkgs => ($EFFECTIVE_USER_ID == 0)
39             );
40              
41 0           GetOptionsFromArray(
42             $args,
43             $config->option_callbacks( $options ),
44             );
45              
46             # normalize $installpath (at least cpanm needs that)
47             # assume $locallib to be inside $installpath
48 0           $config->normalize_paths;
49              
50 0           $class->_post_boot_configure( $dss, $config );
51 0           return ($dss, $config);
52             }
53              
54             my $signing_key_data = <<'KEY_DATA_END';
55             -----BEGIN PGP PUBLIC KEY BLOCK-----
56             Comment: Hostname: fr.pgpkeys.eu
57             Version: Hockeypuck 2.2.4
58              
59             xsBNBEYTN1ABCACuVLUCasiD4KnetfEHtMo+qDjKbcWauIkw/KU+gj8kN7Y+E3ye
60             cLxtY1gJlZp0iIgnZ6aw7bPCleTXbBfXXbMyLs0+CN9cfCyXB5TU4nlh16or/vlE
61             C+0WjEuD9Qznm805RXJGvIRPobXrxcO7aB7pT3NkKj2z//9D0w0uC+BDEl0uihZG
62             +fIbb2ihPWO/r9ghHhyMRTibSRye09owhVpr/5gYRFtvp7OsYHaLhUD3+WXPhGIZ
63             U7YqBFRumDNR+S8EewuKj0CBxCtC+iSFyR/fNPh50FbtgqY/9qLmZa/u42oyjrCl
64             TM+ALCM+PutEPVEIyVoO4jluoZ8yG4IFVxyxABEBAAHNZUxlZGdlclNNQiBDb3Jl
65             IFRlYW0gKFRoaXMgaXMgdGhlIGtleSB1c2VkIGZvciBwYWNrYWdlIHNpZ25pbmcu
66             KSA8bGVkZ2VyLXNtYi1jb3JlQGxpc3RzLmxlZGdlcnNtYi5vcmc+wsB2BBMBAgAg
67             BQJGEzdQAhsDBgsJCAcDAgQVAggDBBYCAwECHgECF4AACgkQOaYpVY2grxAaDwgA
68             kp2LZNyrZDom1tBwR3aisfYFkkiuFrUZUn+fYwr7wnkq06gpYGZrNgdcanSxjGoD
69             wf2kMeCwWAD6sao824onboJWRi1LR3Q5RKIAvYBeLZ75/hYSy3JP/YLMl047IWcU
70             CMaqZfmXPHvkOdUblPxCaKJM8c9Sb6KxGzu4BU2Y97TvQjn9xNbq4t7Pgpu5RORH
71             Hp96V+kZXGxzQo0gCE+6dr1JFZeTKmTkGSvkchndn5UVLguLIg9kCxxcDiS7hmTx
72             yB+b0+kPgRUb0YVp6QqFq7O8fvzhsW8sYopBJsbArODS2qeMiwPUnKs+7qkZg8Jd
73             j2QoHZCY06NU6yvqq/CIt8JGBBARAgAGBQJIx9P/AAoJEHc352JrCredRJwAnRFe
74             UDqpUCw368z/Ht6FDInXnC2kAJ995NXymKbQVg478EdOrJT8h1SPRA==
75             =a/2y
76             -----END PGP PUBLIC KEY BLOCK-----
77             KEY_DATA_END
78              
79 0     0     sub _build_install_tree($class, $dss, $config, $installpath, $version) {
  0            
  0            
  0            
  0            
  0            
  0            
80 0           my $archive = "ledgersmb-$version.tar.gz";
81              
82 0 0         if (not -d $installpath ) {
83 0           $log->info( "Creating installation path $installpath" );
84 0           make_path( $installpath ); # croaks on fatal errors
85             }
86              
87 0           $log->info( "Downloading release tarball $archive" );
88 0           $class->_download( $installpath, $version );
89              
90 0           $log->info( "Verifying tarball against gpg public key & signature" );
91 0           my $downloaded_tar = File::Spec->catfile( $installpath, $archive );
92 0 0         $dss->verify_sig( $installpath,
93             $downloaded_tar,
94             "$downloaded_tar.asc",
95             $signing_key_data )
96             if $config->verify_sig;
97              
98 0           $log->info( "Extracting release tarball" );
99 0           $dss->untar( $downloaded_tar,
100             $installpath,
101             no_same_owner => 1,
102             strip_components => 1 );
103 0           $config->cpanfile_path( File::Spec->catfile( $installpath, 'cpanfile' ) );
104              
105 0           $log->info( "Removing extracted release tarball" );
106             # croaks on fatal errors:
107 0           remove_tree( $downloaded_tar, "$downloaded_tar.asc" );
108             }
109              
110 0     0     sub _get_cpanfile($class, $config) {
  0            
  0            
  0            
111 0 0         return $config->cpanfile if $config->cpanfile;
112              
113 0           my $cpanfile;
114 0 0         if ($config->cpanfile_path) {
115 0           $cpanfile = $config->cpanfile_path;
116             }
117             else {
118 0           my $url =
119             sprintf("https://raw.githubusercontent.com/ledgersmb/LedgerSMB/refs/tags/%s/cpanfile",
120             $config->effective_version);
121              
122 0           my $response = $http->get( $url );
123 0 0         unless ($response->{success}) {
124 0           die $log->fatal("Unable to get '$url' from GitHub: $response->{content}");
125             }
126              
127 0           my ($fh, $fn) = tempfile();
128 0           print $fh $response->{content};
129 0           $fh->flush;
130 0           $cpanfile = $fn;
131             }
132              
133 0           my $decl = Module::CPANfile->load( $cpanfile );
134 0           $config->cpanfile( $decl );
135              
136 0           return $decl;
137             }
138              
139 0     0     sub _get_immediate_prereqs($class, $config) {
  0            
  0            
  0            
140 0           my $decl = $class->_get_cpanfile( $config );
141 0           return $decl->prereqs;
142             }
143              
144 0     0     sub _compute_immediate_deps($class, $config) {
  0            
  0            
  0            
145 0           my @types = qw( requires recommends );
146 0           my @phases = qw( runtime );
147 0           my $decl = $class->_get_cpanfile( $config );
148 0           my $prereqs = $decl->prereqs_with( map { $_->identifier } $decl->features ); # all features
  0            
149 0           my $effective = CPAN::Meta::Requirements->new;
150 0           for my $phase (@phases) {
151 0           for my $type (@types) {
152 0           $effective->add_requirements( $prereqs->requirements_for( $phase, $type ) );
153             }
154             }
155              
156 0           my @mods = sort { lc($a) cmp lc($b) } $effective->required_modules;
  0            
157              
158 0           $log->debug( "Direct dependency count: " . scalar(@mods) );
159 0           $log->trace( "- $_" ) for (sort @mods);
160 0           return sort @mods;
161             }
162              
163 0     0     sub _compute_all_deps($class, $config) {
  0            
  0            
  0            
164 0           my @deps = $class->_compute_immediate_deps( $config );
165              
166 0           my @orig_deps = (@deps, );
167 0           my @last_deps = @deps;
168 0           my %dists;
169 0           my $iteration = 1;
170 0           do {
171 0           my $query = {
172             _source => [ qw( release distribution status provides ), 'dependency.*' ],
173             query => {
174             bool => {
175             must => [
176             { term => { status => 'latest' } },
177             { terms => { provides => [ @last_deps ] } }
178             ],
179             filter => {
180             match_all => {}
181             }
182             }
183             }
184             };
185              
186 0           my $body = $json->encode( $query );
187 0           my $r = $http->request( 'POST', 'https://fastapi.metacpan.org/v1/release/_search?size=1000',
188             { headers => { 'Content-Type' => 'application/json' },
189             content => $body });
190 0           my $hits = $json->decode($r->{content})->{hits};
191              
192 0           for my $release ($hits->{hits}->@*) {
193 0           $dists{$release->{_source}->{distribution}} = 1;
194             }
195              
196 0           my %provide;
197 0           for my $release ($hits->{hits}->@*) {
198 0           for my $provided ($release->{_source}->{provides}->@*) {
199 0           $provide{$provided} = 1;
200             }
201             }
202              
203 0           my %rdeps;
204 0           for my $release ($hits->{hits}->@*) {
205 0           for my $dep ($release->{_source}->{dependency}->@*) {
206 0 0         next unless $dep->{relationship} eq 'requires';
207 0 0         next unless $dep->{phase} eq 'runtime';
208 0           $rdeps{$dep->{module}} = 1;
209             }
210             }
211              
212 0           delete $rdeps{perl};
213             @last_deps = sort grep {
214 0           my $m = $_;
  0            
215 0           my $c = Module::CoreList->is_core($m);
216              
217 0   0       not ($provide{$m} or $c);
218             } keys %rdeps;
219 0           push @deps, @last_deps;
220              
221 0           $log->trace( "Dependency resolution iteration $iteration - "
222             . "remaining to resolve: " . scalar(@last_deps) );
223 0           $iteration++;
224             } while (@last_deps);
225              
226 0           my @missed = do {
227 0           my %deps = map { $_ => 1 } @deps;
  0            
228 0           grep { !$deps{$_} } @orig_deps;
  0            
229             };
230 0 0         $log->error( "Unable to resolve dependencies for modules: "
231             . join(', ', @missed) )
232             if (@missed);
233 0           @deps = sort uniq (@deps, @missed);
234 0           $log->debug( "Dependency tree size: " . scalar(@deps) );
235 0           $log->trace( "- $_" ) for (@deps);
236 0           return @deps;
237             }
238              
239 0     0     sub _compute_dep_pkgs($class, $dss, $config ) {
  0            
  0            
  0            
  0            
240 0           my @mods = $class->_compute_all_deps( $config );
241              
242             # the array passed in gets modified; copy @mods so we still have it below
243 0           my ($pkgs, $unmapped) = $dss->pkgs_from_modules( [ @mods ] );
244              
245 0           my $c = scalar(@mods);
246 0           my $p = scalar(keys $pkgs->%*);
247 0           my $u = scalar($unmapped->@*);
248 0           $log->debug( "Resolved $c modules to $p packages; $u unmapped" );
249 0           return ([ sort keys $pkgs->%* ], $unmapped);
250             }
251              
252 0     0     sub _download($class, $installpath, $version) {
  0            
  0            
  0            
  0            
253 0           my $fn = "ledgersmb-$version.tar.gz";
254 0   0       my $url = $ENV{ARTIFACT_LOCATION} // "https://download.ledgersmb.org/f/Releases/$version/";
255 0           my $http = HTTP::Tiny->new;
256              
257 0           do {
258 0 0         open( my $fh, '>', File::Spec->catfile($installpath, $fn))
259             or croak $log->fatal( "Unable to open output file $fn: $!" );
260 0           binmode $fh, ':raw';
261 0           my $r = $http->get(
262             "$url$fn",
263             {
264 0     0     data_callback => sub($data, $status) {
  0            
  0            
265 0           print $fh $data;
266             }
267 0           });
268              
269 0 0         if ($r->{status} == 599) {
    0          
270 0           croak $log->fatal( "Unable to request $url$fn: " . $r->{content} );
271             }
272             elsif (not $r->{success}) {
273 0           croak $log->fatal( "Unable to request $url$fn: $r->{status} - $r->{reason}" );
274             }
275             };
276              
277 0           do {
278 0           my $r = $http->get( "$url$fn.asc" );
279 0 0         if ($r->{status} == 599) {
    0          
280 0           croak $log->fatal( "Unable to request $url/$fn: " . $r->{content} );
281             }
282             elsif (not $r->{success}) {
283 0           croak $log->fatal( "Unable to request $url/$fn: $r->{status} - $r->{reason}" );
284             }
285             else {
286 0 0         open( my $fh, '>', File::Spec->catfile($installpath, "$fn.asc"))
287             or croak $log->fatal( "Unable to open output file $fn.asc: $!" );
288 0           binmode $fh, ':raw';
289 0           print $fh $r->{content};
290             }
291             };
292             }
293              
294 0     0     sub _find_executable($class, $dss, $executable, $dirs) {
  0            
  0            
  0            
  0            
  0            
295 0           while (my $dir = shift $dirs->@*) {
296 0           my $exe = File::Spec->catfile( $dir, $dss->executable_name( $executable ) );
297 0 0 0       $log->trace( "Found $executable: $exe; but not executable" )
298             if -e $exe and not -x $exe;
299 0 0         my $rv = -x $exe ? $exe : '';
300              
301 0 0         if ($rv) {
302 0           $log->debug( "Searching for $executable; found $exe" );
303 0           return $rv;
304             }
305             }
306 0           return undef;
307             }
308              
309             # This function is borrowed from App::Info::RDBMS::PostgreSQL v0.57
310             # because that is what DBD::Pg uses to identify where pg_config lives
311 0     0     sub _search_bin_dirs($class) {
  0            
  0            
312             return (( exists $ENV{POSTGRES_HOME}
313             ? (File::Spec->catdir($ENV{POSTGRES_HOME}, "bin"))
314             : ()
315             ),
316             ( exists $ENV{POSTGRES_LIB}
317 0 0         ? (File::Spec->catdir($ENV{POSTGRES_LIB}, File::Spec->updir, "bin"))
    0          
318             : ()
319             ),
320             File::Spec->path,
321             qw(/usr/local/pgsql/bin
322             /usr/local/postgres/bin
323             /usr/lib/postgresql/bin
324             /opt/pgsql/bin
325             /usr/local/bin
326             /usr/local/sbin
327             /usr/bin
328             /usr/sbin
329             /bin),
330             'C:\Program Files\PostgreSQL\bin');
331             }
332             # end of borrowed code
333              
334 0     0     sub _find_pg_config($class, $dss, $config) {
  0            
  0            
  0            
  0            
335 0           my @dirs = $class->_search_bin_dirs;
336              
337             # TODO: Check for pg_config in $config
338              
339 0           return $class->_find_executable( $dss, 'pg_config', \@dirs );
340             }
341              
342 0     0     sub _find_xml2_config($class, $dss, $config) {
  0            
  0            
  0            
  0            
343 0           return $class->_find_executable( $dss, 'xml2-config', [ File::Spec->path ] );
344             }
345              
346 0     0     sub _find_latex($class, $dss, $config) {
  0            
  0            
  0            
  0            
347 0           return $class->_find_executable( $dss, 'latex', [ File::Spec->path ] );
348             }
349              
350              
351             # mapping taken from File::Spec
352             my %module = (
353             MSWin32 => 'win32',
354             os2 => 'os2',
355             VMS => 'vms',
356             NetWare => 'win32',
357             symbian => 'win32',
358             dos => 'os2',
359             cygwin => 'cygwin',
360             amigaos => 'amigaos',
361             linux => 'linux' # not mapped in File::Spec
362             );
363              
364 0     0     sub _get_os($class) {
  0            
  0            
365 0   0       return $module{$^O} || 'unix';
366             }
367              
368 0     0     sub _load_dist_support($class) {
  0            
  0            
369 0           my $OS = $class->_get_os;
370              
371 0           $log->info( "Detected O/S: $OS" );
372 0           my $oss_class = "LedgerSMB::Installer::OS::$OS";
373              
374 0           local $@ = undef;
375 0 0         unless (eval "require $oss_class") {
376 0           say "Unable to load $oss_class: $@";
377 0           say "No support for $OS";
378 0           exit 2;
379             }
380              
381 0           my $oss = $oss_class->new; # operating system support instance
382 0           $log->debug( "Detecting distribution" );
383 0           return $oss->detect_dss; # detect and return distribution support instance
384             }
385              
386 0     0 0   sub compute($class, @args) {
  0            
  0            
  0            
387 0           my ($dss, $config) = $class->_boot(
388             \@args,
389             [ 'yes|y!', 'target=s', 'local-lib=s', 'log-level=s' ]
390             );
391              
392 0 0         if (@args != 2) {
393 0           die "Incorrect number of arguments";
394             }
395              
396 0           my $version = shift @args;
397 0           $config->compute_deps( 1 );
398 0 0         $config->version( $version ) if defined $version;
399              
400 0           my $deps_outfile = $args[0];
401 0 0         open( my $out, '>:raw', $deps_outfile )
402             or die "Unable to open output file '$deps_outfile': $!";
403              
404              
405 0 0         unless ($dss->am_system_perl) {
406 0 0         close( $out ) or warn $log->warn( "Unable to close output file" );
407 0           unlink $deps_outfile;
408 0           die $log->fatal( "Not running the system perl; not able to re-use system packages" );
409             }
410              
411             ###TODO: _get_immediate_prereqs may throw
412 0           my $prereqs = $class->_get_immediate_prereqs( $config );
413 0           my $requirements = $prereqs->merged_requirements();
414 0 0         unless ($requirements->accepts_module( 'perl', $])) {
415 0 0         close( $out ) or warn $log->warn( "Unable to close output file" );
416 0           unlink $deps_outfile;
417 0           my $perl_version = version->parse( $] )->normal;
418 0           die $log->fatal( "Perl version ($perl_version) not compliant with LedgerSMB " . $config->effective_version
419             . "; requires: " . $requirements->requirements_for_module( 'perl' ));
420             }
421              
422             ###TODO: prepare_pkg_resolver_env may throw
423 0 0         if ($config->effective_prepare_env) {
424 0           $dss->prepare_pkg_resolver_env( $config );
425             }
426 0           my $exception;
427 0           do {
428 0           local $@ = undef;
429 0           my $failed = not eval {
430 0           $log->info( "Computing O/S packages for declared dependencies" );
431 0           my ($deps, $mods) = $class->_compute_dep_pkgs( $dss, $config );
432              
433 0           say $out $json->encode( { identifier => $dss->dependency_packages_identifier,
434             packages => $deps,
435             modules => $mods,
436             name => $dss->name,
437             'schema-version' => "1" } );
438              
439 0           return 1;
440             };
441 0 0         $exception = $@ if $failed;
442              
443 0           $log->info( "Dependencies written to $deps_outfile" );
444 0 0         if ($config->effective_uninstall_env) {
445 0           $log->warning( "Cleaning up Perl module installation dependencies" );
446 0           $dss->cleanup_env($config);
447             }
448             };
449 0 0         die $exception if defined $exception;
450              
451 0           return 0;
452             }
453              
454             sub download($class, @args) {
455             }
456              
457 0     0 0   sub help($class, @args) {
  0            
  0            
  0            
458 0           my $help_text = do {
459 0           local $/ = undef;
460 0           ;
461             };
462 0           $help_text =~ s/\bSCRIPT\b/$0/g;
463 0           say $help_text;
464              
465 0           return 0;
466             }
467              
468 0     0     sub _module_will_install($class, $mod, $pkgs, $unmapped) {
  0            
  0            
  0            
  0            
  0            
469             # no packages will be installed:
470 0 0 0       return 0 if ((not $pkgs) or (not $pkgs->@*));
471              
472             # package in the list of those not installed through a package:
473 0 0         return 0 if (grep { $mod eq $_ } $unmapped->@*);
  0            
474              
475             # package must be in one of the packages being installed
476 0           return 1;
477             }
478              
479 0     0 0   sub install($class, @args) {
  0            
  0            
  0            
480 0           my $rv = 1;
481 0           my ($dss, $config) = $class->_boot(
482             \@args,
483             [ 'yes|y!', 'system-packages!', 'prepare-env!', 'target=s',
484             'local-lib=s', 'log-level=s', 'verify-sig!' ]
485             );
486 0           my $version = shift @args;
487 0 0         $config->version( $version ) if defined $version;
488              
489 0           my ($pkg_deps, $unmapped_mods);
490 0           my @extra_pkgs;
491 0 0         if ($dss->am_system_perl) {
492 0           my $name = $dss->name;
493 0           my $dep_pkg_id = $dss->dependency_packages_identifier;
494 0 0         if ($config->sys_pkgs) {
495 0           ($pkg_deps, $unmapped_mods) = $config->retrieve_precomputed_deps($name, $dep_pkg_id);
496             }
497 0 0         if ($pkg_deps) {
498 0 0         if ($dss->pkg_can_install()) {
499 0 0         if ($config->effective_prepare_env) {
500 0           $dss->prepare_builder_env( $config );
501             }
502 0           goto INSTALL_SYS_PKGS;
503             }
504             else {
505 0           $log->warn( "Unable to install system packages; will resort to installation of CPAN modules" );
506 0           $pkg_deps = undef;
507             }
508             }
509             else {
510 0           $log->warn( "No precomputed dependencies available for $name/$dep_pkg_id" );
511 0           $log->info( "Configuring environment for dependency computation" );
512             }
513             }
514              
515             ########################################################################################
516             #
517             # Need to clean up on failure after this point! We have changed system state!
518             #
519             ########################################################################################
520 0           $dss->prepare_extraction_env( $config );
521 0           $class->_build_install_tree( $dss, $config, $config->installpath, $config->effective_version );
522 0           my $prereqs = $class->_get_immediate_prereqs( $config );
523 0           my $requirements = $prereqs->merged_requirements();
524              
525 0 0         unless ($requirements->accepts_module( 'perl', $])) {
526             # BAIL: No suitable Perl here...
527             #
528             # well, we might want to see if Perlbrew is installed with an acceptable version?
529             #
530             # and if not, we could install Perlbrew here...
531 0           die $log->fatal( "Not running a Perl version compliant with LedgerSMB " . $config->effective_version );
532             }
533              
534 0 0         if ($config->effective_prepare_env) {
535 0           $dss->prepare_builder_env( $config );
536             }
537              
538 0 0 0       if ($dss->am_system_perl and $dss->pkg_can_install) { # and $dss->deps_can_map
539 0 0         if ($config->effective_prepare_env) {
540 0           $dss->prepare_pkg_resolver_env( $config );
541             }
542 0           ($pkg_deps, $unmapped_mods) = $class->_compute_dep_pkgs( $dss, $config );
543             }
544             else {
545 0           $unmapped_mods = [ $class->_compute_all_deps( $config ) ];
546             }
547              
548 0           $log->info( "Checking for availability of DBD::Pg" );
549 0 0 0       if (not eval { require DBD::Pg; 1; } # not loadable, and
    0          
    0          
550             and not $class->_module_will_install( 'DBD::Pg', $pkg_deps, $unmapped_mods )) {
551             # don't have DBD::Pg
552             # *and* won't install as part of $pkg_deps
553              
554 0           my $pg_config = $class->_find_pg_config( $dss, $config );
555 0 0         die $log->fatal( "Missing 'pg_config' command to build DBD::Pg" )
556             unless $pg_config;
557 0           chomp( my $include_dir = `'$pg_config' --includedir` );
558              
559 0           $log->debug( "Directory for PostgreSQL headers: $include_dir" );
560 0           my $header_file = File::Spec->catfile( $include_dir, 'libpq-fe.h' );
561              
562 0 0         if (not -e $header_file) {
563 0 0         if (not $dss->pkg_can_install) {
564 0           die $log->fatal( "Missing 'libpq-fe.h' PostgreSQL header to build DBD::Pg" );
565             }
566              
567 0           my ($run_deps, $build_deps) = $dss->pkg_deps_dbd_pg;
568 0           $config->mark_pkgs_for_cleanup( $build_deps );
569 0           push @extra_pkgs, $run_deps->@*, $build_deps->@*;
570             }
571             }
572 0           elsif (eval { require DBD::Pg; 1; }) {
  0            
573 0           $log->info( "DBD::Pg is loadable" );
574             }
575 0           elsif (not grep { $_ eq 'DBD::Pg' } $unmapped_mods->@*) {
576 0           $log->info( "DBD::Pg will be installed" );
577             }
578             else {
579 0           $log->fatal( "Internal error: DBD::Pg not available and won't be installed, but build prereqs not checked?!?!" );
580             }
581              
582 0           $log->info( "Checking for availability of LaTeX::Driver" );
583 0 0 0       if (not eval { require LaTeX::Driver; 1; } # loadable, and
    0          
    0          
584             and not $class->_module_will_install( 'LaTeX::Driver', $pkg_deps, $unmapped_mods )) {
585             # don't have LaTeX::Driver
586             # *and* won't install as part of $pkg_deps
587              
588             # testing early, because LaTeX::Driver only installs
589             # when LaTeX is installed...
590              
591 0           my $latex = $class->_find_latex( $dss, $config );
592 0 0         if (not $latex) {
593 0 0         if (not $dss->pkg_can_install) {
594 0           die $log->fatal( "Missing 'latex' executable required for building 'LaTeX::Driver' module" );
595             }
596             }
597             }
598 0           elsif (eval { require LaTeX::Driver; 1; }) {
  0            
599 0           $log->info( "LaTeX::Driver is loadable" );
600             }
601 0           elsif (not grep { $_ eq 'LaTeX::Driver' } $unmapped_mods->@*) {
602 0           $log->info( "LaTeX::Driver will be installed" );
603             }
604             else {
605 0           $log->fatal( "Internal error: LaTeX::Driver not available and won't be installed, but build prereqs not checked?!?!" );
606             }
607 0 0         if ($dss->pkg_can_install) {
608             # installs LaTeX *and* styles required for standard templates
609 0           my ($run_deps, $build_deps) = $dss->pkg_deps_latex;
610 0           $config->mark_pkgs_for_cleanup( $build_deps );
611 0           push @extra_pkgs, $run_deps->@*, $build_deps->@*;
612             }
613             else {
614             ###TODO: figure out how to warn the user of minimal required
615             # styles configuration...
616             }
617              
618 0           $log->info( "Checking for availability of XML::Parser" );
619 0 0 0       if (not eval { require XML::Parser; 1; }
    0          
    0          
620             and not $class->_module_will_install( 'XML::Parser', $pkg_deps, $unmapped_mods)) {
621              
622 0           $log->info( "Checking availability of libexpat" );
623 0   0       my $incpath = $ENV{EXPATINCPATH} // '';
624 0   0       my $libpath = $ENV{EXPATLIBPATH} // '';
625              
626             # Devel::CheckLib tries to find the C compiler when use-d
627             # however, we might be installing it as part of the installation
628             # process, so we don't want Devel::CheckLib to check when loading the script...
629 0           eval "use Devel::CheckLib qw(assert_lib);";
630 0 0         unless (eval { assert_lib( lib => [qw(expat)],
  0 0          
631             header => ['expat.h'],
632             incpath => $incpath,
633             ( $libpath ? (libpath => $libpath) : () )
634 0           ); 1; }) {
635 0 0         if (not $dss->pkg_can_install) {
636             ###TODO: We could push Alien::Expat into @unmapped_mods
637             # instead of bailing out here...
638 0           die $log->fatal( "Missing XML parser library Expat blocking installation of XML::Parser" );
639             }
640             else {
641 0           my ($run_deps, $build_deps) = $dss->pkg_deps_expat;
642 0           $config->mark_pkgs_for_cleanup( $build_deps );
643 0           push @extra_pkgs, $run_deps->@*, $build_deps->@*;
644             }
645             }
646             else {
647 0           $log->info( "Found expat header and library for XML::Parser compilation" );
648             }
649             }
650 0           elsif (eval { require XML::Parser; 1; }) {
  0            
651 0           $log->info( "XML::Parser is loadable" );
652             }
653 0           elsif (not grep { $_ eq 'XML::Parser' } $unmapped_mods->@*) {
654 0           $log->info( "XML::Parser will be installed" );
655             }
656             else {
657 0           $log->fatal( "Internal error: XML::Parser not available and won't be installed, but build prereqs not checked?!?!" );
658             }
659              
660 0 0 0       if ((not eval { require XML::LibXML; 1; }
  0   0        
  0   0        
661             and not $class->_module_will_install( 'XML::LibXML', $pkg_deps, $unmapped_mods ))
662 0           and (not eval { require XML::Twig; 1; }
  0            
663             and not $class->_module_will_install( 'XML::Twig', $pkg_deps, $unmapped_mods ))) {
664             # don't have either XML::LibXML or XML::Twig
665              
666 0           my $xml2_config = $class->_find_xml2_config( $dss, $config );
667 0 0         if (not $xml2_config) {
668 0 0         if (not $dss->pkg_can_install) {
669 0           warn $log->warning("Missing 'xml2-config' executable required for building XML::LibXML -- falling back to Alien::Libxml2" );
670             }
671             else {
672 0           my ($run_deps, $build_deps) = $dss->pkg_deps_xml;
673 0           $config->mark_pkgs_for_cleanup( $build_deps );
674 0           push @extra_pkgs, $run_deps->@*, $build_deps->@*;
675             }
676             }
677             }
678              
679 0 0 0       goto PREPARE_TREE if (not $pkg_deps or not $pkg_deps->@*);
680              
681 0           INSTALL_SYS_PKGS:
682             $log->info( "Installing O/S packages: " . join(' ', $pkg_deps->@*) );
683 0           $dss->pkg_install( $pkg_deps );
684              
685             PREPARE_TREE:
686 0 0         if (@extra_pkgs) {
687 0           $log->info( "Installing build dependency packages: " . join(' ', @extra_pkgs) );
688 0           $dss->pkg_install( \@extra_pkgs );
689             }
690 0 0         if ($config->effective_prepare_env) {
691 0           $dss->prepare_installer_env( $config );
692             }
693              
694             ###TODO: ideally, we pass the immediate dependencies instead of the installation path;
695             # that allows selection of specific features in a later iteration
696             #
697             # Installs dependencies from cpanfile (because it respects dependency versions,
698             # which the simple list of dependencies does not); then installs the extra
699             # dependencies which were not satisfied from the list of unmapped_mods)
700 0           $dss->cpanm_install( $config->installpath, $config->locallib, $unmapped_mods );
701 0           $rv = 0;
702              
703 0           $log->info( "Generating application server startup script (server-start)" );
704 0           $dss->generate_start_script( File::Spec->rel2abs( $config->installpath ),
705             File::Spec->rel2abs( $config->locallib ) );
706              
707 0           CLEANUP:
708             $log->warning( "Cleaning up Perl module installation dependencies" );
709 0           $dss->cleanup_env($config);
710              
711 0 0         if ($rv) {
712 0 0         if (-e $config->installpath) {
713 0           $log->warning( "Cleaning up installation path" );
714 0           remove_tree $config->installpath;
715             }
716              
717 0           say "Failed to complete server installation.";
718             }
719             else {
720 0           my $installpath = $config->installpath;
721 0           say "
722             LedgerSMB installation into $installpath completed.
723              
724             The LedgerSMB server can be started using the generated
725             startup script:
726              
727             \$ $installpath/server-start
728              
729             The server will run with its default configuration. To change
730             the configuration, copy the file doc/conf/ledgersmb.yaml to
731             $installpath and modify it. After modification, restart the
732             LedgerSMB server to activate the new configuration.
733              
734             ";
735             }
736              
737 0           return $rv;
738             }
739              
740 0     0 0   sub print_id( $class, @args) {
  0            
  0            
  0            
741 0           my $dss = $class->_load_dist_support;
742 0           say $dss->dependency_packages_identifier;
743             }
744              
745 0     0 0   sub run($class, $cmd, @args) {
  0            
  0            
  0            
  0            
746 0           STDOUT->autoflush(1);
747 0           STDERR->autoflush(1);
748              
749 0 0         if ($cmd =~ m/^-/) { # option(s)
750 0           unshift @args, $cmd;
751 0           $cmd = 'install';
752             }
753              
754 0 0         if ($cmd eq 'compute') {
    0          
    0          
    0          
    0          
755 0           say $log->info( "Computing dependencies using installer $INSTALLER_VERSION" );
756 0           return $class->compute( @args );
757             }
758             elsif ($cmd eq 'download') {
759 0           return $class->download( @args );
760             }
761             elsif ($cmd eq 'help') {
762 0           return $class->help( @args );
763             }
764             elsif ($cmd eq 'install') {
765 0           say $log->info( "Installing LedgerSMB using installer $INSTALLER_VERSION" );
766 0           return $class->install( @args );
767             }
768             elsif ($cmd eq 'system-id') {
769 0           return $class->print_id( @args );
770             }
771             else {
772 0           $class->help();
773 0           exit 1;
774             }
775             }
776              
777              
778             1;
779              
780             =head1 NAME
781              
782             LedgerSMB::Installer - Installer for the LedgerSMB accounting web application
783              
784             =head1 DESCRIPTION
785              
786             =head1 MAINTAINER
787              
788             Erik Huelsmann
789              
790             =head1 AUTHOR
791              
792             Erik Huelsmann
793              
794             =head1 LICENSE
795              
796             Copyright 2025 Erik Huelsmann
797              
798             Permission is hereby granted, free of charge, to any person obtaining a copy
799             of this software and associated documentation files (the "Software"), to deal
800             in the Software without restriction, including without limitation the rights
801             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
802             copies of the Software, and to permit persons to whom the Software is
803             furnished to do so, subject to the following conditions:
804              
805             The above copyright notice and this permission notice shall be included in
806             all copies or substantial portions of the Software.
807              
808             THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
809             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
810             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
811             THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
812             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
813             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
814             SOFTWARE.
815              
816             =cut
817              
818             __DATA__