File Coverage

blib/lib/App/Rakubrew/VersionHandling.pm
Criterion Covered Total %
statement 26 270 9.6
branch 0 148 0.0
condition 0 105 0.0
subroutine 9 34 26.4
pod 0 24 0.0
total 35 581 6.0


line stmt bran cond sub pod time code
1             package App::Rakubrew::VersionHandling;
2             require Exporter;
3             our @ISA = qw( Exporter );
4             our @EXPORT = qw(
5             get_versions
6             get_version
7             version_exists
8             verify_version
9             is_version_broken is_version_path_broken
10             is_registered_version
11             get_version_path clean_version_path
12             get_shell_version
13             get_local_version set_local_version
14             get_global_version set_global_version
15             set_brew_mode get_brew_mode get_brew_mode_shell validate_brew_mode
16             get_raku
17             which whence
18             get_bin_paths
19             rehash
20             );
21              
22 3     3   22 use strict;
  3         5  
  3         84  
23 3     3   14 use warnings;
  3         17  
  3         77  
24 3     3   47 use 5.010;
  3         9  
25 3     3   29 use File::Spec::Functions qw(catfile catdir splitdir splitpath catpath canonpath);
  3         5  
  3         204  
26 3     3   25 use Cwd qw(realpath);
  3         14  
  3         119  
27 3     3   17 use File::Which qw();
  3         6  
  3         100  
28 3     3   1099 use Try::Tiny;
  3         4041  
  3         153  
29 3     3   19 use App::Rakubrew::Variables;
  3         6  
  3         434  
30 3     3   20 use App::Rakubrew::Tools;
  3         6  
  3         9130  
31              
32             sub get_versions {
33 0     0 0   opendir(my $dh, $versions_dir);
34             my @versions = (
35             'system',
36 0           sort({ $a cmp $b }
37 0           grep({ /^[^.]/ } readdir($dh)))
  0            
38             );
39 0           closedir($dh);
40 0           return @versions;
41             }
42              
43             sub get_shell_version {
44             # Check for shell version by looking for $RAKU_VERSION or $PL6ENV_VERSION the environment.
45 0 0 0 0 0   if (defined $ENV{$env_var} || defined $ENV{PL6ENV_VERSION}) {
46 0   0       my $version = $ENV{$env_var} // $ENV{PL6ENV_VERSION};
47 0 0         if (version_exists($version)) {
48 0           return $version;
49             }
50             else {
51 0           say STDERR "Version '$version' is set via the RAKU_VERSION environment variable.";
52 0           say STDERR "This version is not installed. Ignoring.";
53 0           say STDERR '';
54 0           return undef;
55             }
56             }
57             else {
58 0           return undef;
59             }
60             }
61              
62             sub get_local_version {
63 0     0 0   my ($vol, $path, undef) = splitpath(realpath(), 1);
64 0           my @fragments = splitdir($path);
65 0           while (@fragments) {
66 0           for ($local_filename, '.perl6-version') {
67 0           my $filepath = catpath($vol, catdir(@fragments), $_);
68 0 0         if (-f $filepath) {
69 0           my $version = trim(slurp($filepath));
70 0 0         if(version_exists($version)) {
71 0           return $version;
72             }
73             else {
74 0           say STDERR "Version '$version' is given in the";
75 0           say STDERR "$filepath";
76 0           say STDERR "file. This version is not installed. Ignoring.";
77 0           say STDERR '';
78             }
79             }
80             }
81 0           pop @fragments;
82             }
83 0           return undef;
84             }
85              
86             sub is_version_broken {
87 0     0 0   my $version = shift;
88 0 0         return 0 if $version eq 'system';
89 0           my $path = get_version_path($version, 1);
90 0 0         return 1 if !$path;
91 0 0         return 0 if !is_version_path_broken($path);
92 0           return 1;
93             }
94              
95             sub is_version_path_broken {
96 0     0 0   my $path = shift;
97 0           $path = clean_version_path($path);
98 0 0         return 1 if !$path;
99 0           for my $exec ('raku', 'raku.bat', 'raku.exe', 'perl6', 'perl6.bat', 'perl6.exe', 'rakudo', 'rakudo.bat', 'rakudo.exe') {
100 0 0         if (-f catfile($path, 'bin', $exec)) {
101 0           return 0;
102             }
103             }
104 0           return 1;
105             }
106              
107             sub verify_version {
108 0     0 0   my $version = shift;
109              
110 0 0         if (! version_exists($version) ) {
111 0           say STDERR "$brew_name: version '$version' is not installed.";
112 0           exit 1;
113             }
114              
115 0 0         if ( is_version_broken($version) ) {
116 0           say STDERR "Version $version is broken. Refusing to switch to it.";
117 0           exit 1;
118             }
119             }
120              
121             sub set_local_version {
122 0     0 0   my $version = shift;
123 0 0         if ($version) {
124 0           verify_version($version);
125 0           spurt($local_filename, $version);
126             }
127             else {
128 0           unlink $local_filename;
129 0           unlink '.perl6-version';
130             }
131             }
132              
133             sub get_global_version {
134 0 0   0 0   if (!-e catfile($prefix, 'CURRENT')) {
135 0           set_global_version('system', 1);
136             }
137 0           my $cur = slurp(catfile($prefix, 'CURRENT'));
138 0           chomp $cur;
139 0           return $cur;
140             }
141              
142             sub set_global_version {
143 0     0 0   my $version = shift;
144 0           my $silent = shift;
145 0           verify_version($version);
146 0 0         say "Switching to $version" unless $silent;
147 0           spurt(catfile($prefix, 'CURRENT'), $version);
148             }
149              
150             sub get_version {
151 0   0 0 0   my $ignore = shift // '';
152 0 0         my $version = $ignore eq 'shell' ? undef : get_shell_version();
153 0 0         return $version if defined $version;
154            
155 0 0         if (get_brew_mode() eq 'shim') {
156             # Local version is only supported in shim mode.
157             # Check for local version by looking for a `.raku-version` file in the current and parent folders.
158 0 0         $version = $ignore eq 'local' ? undef : get_local_version();
159 0 0         return $version if defined $version;
160             }
161              
162             # Check for global version by looking at `$prefix/CURRENT` (`$prefix/version`)
163 0           return get_global_version();
164             }
165              
166             sub set_brew_mode {
167 0     0 0   my $mode = shift;
168 0 0         if ($mode eq 'env') {
    0          
169 0           spurt(catfile($prefix, 'MODE'), 'env');
170             }
171             elsif ($mode eq 'shim') {
172 0           spurt(catfile($prefix, 'MODE'), 'shim');
173 0           rehash();
174             }
175             else {
176 0           say STDERR "Mode must either be 'env' or 'shim'";
177             }
178             }
179              
180             sub get_brew_mode {
181 0     0 0   my $silent = shift;
182 0 0         if (!-e catfile($prefix, 'MODE')) {
183 0           spurt(catfile($prefix, 'MODE'), 'env');
184             }
185              
186 0           my $mode = trim(slurp(catfile($prefix, 'MODE')));
187              
188 0 0 0       if ($mode ne 'env' && $mode ne 'shim') {
189 0 0         say STDERR 'Invalid mode found: ' . $mode unless $silent;
190 0 0         say STDERR 'Resetting to env-mode' unless $silent;
191 0           set_brew_mode('env');
192 0           $mode = 'env';
193             }
194              
195 0           return $mode;
196             }
197              
198             sub validate_brew_mode {
199 0 0   0 0   if (get_brew_mode() eq 'env') {
200 0           say STDERR "This command is not available in 'env' mode. Switch to to 'shim' mode using '$brew_name mode shim'";
201 0           exit 1;
202             }
203             }
204              
205             sub version_exists {
206 0     0 0   my $version = shift;
207 0 0         return undef if !defined $version;
208 0           my %versionsMap = map { $_ => 1 } get_versions();
  0            
209 0           return exists($versionsMap{$version});
210             }
211              
212             sub is_registered_version {
213 0     0 0   my $version = shift;
214 0           my $version_file = catdir($versions_dir, $version);
215 0 0         if (-f $version_file) {
216 0           return 1;
217             }
218             else {
219 0           return 0;
220             }
221             }
222              
223             sub clean_version_path {
224 0     0 0   my $path = shift;
225              
226 0           my @cands = (catdir($path, 'install'), $path);
227 0           for my $cand (@cands) {
228 0 0         return $cand if -d catdir($cand, 'bin')
229             }
230 0           return undef;
231             }
232              
233             sub get_version_path {
234 0     0 0   my $version = shift;
235 0   0       my $no_error = shift || 0;
236 0           my $version_path = catdir($versions_dir, $version);
237 0 0         $version_path = trim(slurp($version_path)) if -f $version_path;
238              
239 0           $version_path = clean_version_path($version_path);
240 0 0 0       return $version_path if $version_path || $no_error;
241 0           die "Installation is broken: $version";
242             }
243              
244             sub get_raku {
245 0     0 0   my $version = shift;
246              
247 0   0       return _which('raku', $version) // which('perl6', $version);
248             }
249              
250             sub match_version {
251 0   0 0 0   my $impl = shift // 'moar';
252 0 0 0       my $ver = shift if @_ && $_[0] !~ /^--/;
253 0           my @args = @_;
254              
255 0 0         if (!defined $ver) {
256 0           my $version_regex = '^\d\d\d\d\.\d\d(?:\.\d+)?$';
257 0           my $combined_regex = '('
258             . join('|', App::Rakubrew::Variables::available_backends())
259             . ')-(.+)';
260 0 0         if ($impl eq 'moar-blead') {
    0          
    0          
261 0           $ver = 'main';
262             }
263             elsif ($impl =~ /$combined_regex/) {
264 0           $impl = $1;
265 0           $ver = $2;
266             }
267             elsif ($impl =~ /$version_regex/) {
268 0           $ver = $impl;
269 0           $impl = 'moar';
270             }
271             else {
272 0           $ver = '';
273             }
274             }
275              
276 0           return ($impl, $ver, @args);
277             }
278              
279             sub which {
280 0     0 0   my $prog = shift;
281 0           my $version = shift;
282              
283 0           my $target = _which($prog, $version);
284              
285 0 0         if (!$target) {
286 0           say STDERR "$brew_name: $prog: command not found";
287 0 0         if(whence($prog)) {
288 0           say STDERR <
289              
290             The '$prog' command exists in these Raku versions:
291             EOT
292 0           map {say STDERR $_} whence($prog);
  0            
293             }
294 0           exit 1;
295             }
296              
297 0           return $target;
298             }
299              
300             sub _which {
301 0     0     my $prog = shift;
302 0           my $version = shift;
303              
304 0           my $target; {
305 0 0 0       if ($version eq 'system') {
  0 0          
306 0           my @targets = File::Which::which($prog);
307             @targets = map({
308 0           $_ =~ s|\\|/|g;
  0            
309 0           $_ = canonpath($_);
310             } @targets);
311              
312 0           my $normalized_shim_dir = $shim_dir;
313 0           $normalized_shim_dir =~ s|\\|/|g;
314 0           $normalized_shim_dir = canonpath($normalized_shim_dir);
315              
316             @targets = grep({
317 0           my ($volume,$directories,$file) = splitpath( $_ );
  0            
318 0           my $target_dir = catpath($volume, $directories);
319 0           $target_dir = canonpath($target_dir);
320 0           $target_dir ne $normalized_shim_dir;
321             } @targets);
322              
323 0 0         $target = $targets[0] if @targets;
324             }
325             elsif ($^O =~ /win32/i && (my_fileparse($prog))[2] eq '') {
326             # If we are on Windows and didn't get a full executable name
327             # i.e. the suffix is missing.
328             # In this case we look for files with a basename matching
329             # the given name and select the best candidate via a preference
330             # table.
331              
332             sub check_prog_name_match {
333 0     0 0   my ($prog, $filename) = @_;
334 0           my ($basename, undef, undef) = my_fileparse($filename);
335 0           return $prog =~ /^\Q$basename\E\z/i;
336             }
337              
338 0           my @results = ();
339 0           my @dirs = get_bin_paths($version);
340 0           for my $dir (@dirs) {
341 0           my @files = slurp_dir($dir);
342 0           for my $file (@files) {
343 0 0         if(check_prog_name_match($prog, $file)) {
344 0           push @results, catfile($dir, $file);
345             }
346             }
347             }
348             @results = sort {
349             # .exe > .bat > .raku > .p6 > .pl6 > .pl > nothing > rest
350 0           my (undef, undef, $suffix_a) = my_fileparse($a);
  0            
351 0           my (undef, undef, $suffix_b) = my_fileparse($b);
352 0 0 0       return -1 if $suffix_a eq '.exe' && $suffix_b ne '.exe';
353 0 0 0       return 1 if $suffix_a ne '.exe' && $suffix_b eq '.exe';
354 0 0 0       return $a cmp $b if $suffix_a eq '.exe' && $suffix_b eq '.exe';
355 0 0 0       return -1 if $suffix_a eq '.bat' && $suffix_b ne '.bat';
356 0 0 0       return 1 if $suffix_a ne '.bat' && $suffix_b eq '.bat';
357 0 0 0       return $a cmp $b if $suffix_a eq '.bat' && $suffix_b eq '.bat';
358 0 0 0       return -1 if $suffix_a eq '.raku' && $suffix_b ne '.raku';
359 0 0 0       return 1 if $suffix_a ne '.raku' && $suffix_b eq '.raku';
360 0 0 0       return $a cmp $b if $suffix_a eq '.raku' && $suffix_b eq '.raku';
361 0 0 0       return -1 if $suffix_a eq '.p6' && $suffix_b ne '.p6';
362 0 0 0       return 1 if $suffix_a ne '.p6' && $suffix_b eq '.p6';
363 0 0 0       return $a cmp $b if $suffix_a eq '.p6' && $suffix_b eq '.p6';
364 0 0 0       return -1 if $suffix_a eq '.pl6' && $suffix_b ne '.pl6';
365 0 0 0       return 1 if $suffix_a ne '.pl6' && $suffix_b eq '.pl6';
366 0 0 0       return $a cmp $b if $suffix_a eq '.pl6' && $suffix_b eq '.pl6';
367 0 0 0       return -1 if $suffix_a eq '.pl' && $suffix_b ne '.pl';
368 0 0 0       return 1 if $suffix_a ne '.pl' && $suffix_b eq '.pl';
369 0 0 0       return $a cmp $b if $suffix_a eq '.pl' && $suffix_b eq '.pl';
370 0 0 0       return -1 if $suffix_a eq '' && $suffix_b ne '';
371 0 0 0       return 1 if $suffix_a ne '' && $suffix_b eq '';
372 0 0 0       return $a cmp $b if $suffix_a eq '' && $suffix_b eq '';
373 0           return $a cmp $b;
374             } @results;
375 0           $target = $results[0];
376             }
377             else {
378 0           my @paths = get_bin_paths($version, $prog);
379 0           for my $path (@paths) {
380 0 0         if (-e $path) {
381 0           $target = $path;
382 0           last;
383             }
384             }
385             }
386             }
387              
388 0           return $target;
389             }
390              
391             sub whence {
392 0     0 0   my $prog = shift;
393 0   0       my $pathmode = shift // 0;
394              
395 0           my @matches = ();
396 0           for my $version (get_versions()) {
397 0 0         next if $version eq 'system';
398 0 0         next if is_version_broken($version);
399 0           for my $path (get_bin_paths($version, $prog)) {
400 0 0         if (-f $path) {
401 0 0         if ($pathmode) {
402 0           push @matches, $path;
403             }
404             else {
405 0           push @matches, $version;
406             }
407 0           last;
408             }
409             }
410             }
411 0           return @matches;
412             }
413              
414             sub get_bin_paths {
415 0     0 0   my $version = shift;
416 0   0       my $program = scalar(shift) || undef;
417 0   0       my $no_error = shift || undef;
418 0           my $version_path = get_version_path($version, 1);
419 0 0 0       return () if $no_error && !$version_path;
420              
421             return (
422 0   0       catfile($version_path, 'bin', $program // ()),
      0        
423             catfile($version_path, 'share', 'perl6', 'site', 'bin', $program // ()),
424             );
425             }
426              
427             sub rehash {
428 0 0   0 0   return if get_brew_mode() ne 'shim';
429              
430 0           my @paths = ();
431 0           for my $version (get_versions()) {
432 0 0         next if $version eq 'system';
433 0 0         next if is_version_broken($version);
434 0           push @paths, get_bin_paths($version);
435             }
436              
437 0           say "Updating shims";
438              
439             { # Remove the existing shims.
440 0           opendir(my $dh, $shim_dir);
  0            
441 0           while (my $entry = readdir $dh) {
442 0 0         next if $entry =~ /^\./;
443 0           unlink catfile($shim_dir, $entry);
444             }
445 0           closedir $dh;
446             }
447              
448 0           my @bins = map { slurp_dir($_) } @paths;
  0            
449              
450 0 0         if ($^O =~ /win32/i) {
451             # This wrapper is needed because:
452             # - We want rakubrew to work even when the .pl ending is not associated with the perl program and we do not want to put `perl` before every call to a shim.
453             # - exec() in perl on Windows behaves differently from running the target program directly (output ends up on the console differently).
454             # It retrieves the target executable (only consuming STDOUT of rakubrew) and calls it with the given arguments. STDERR still ends up on the console. The return value is checked and if an error occurs that error values is returned.
455             # `IF ERRORLEVEL 1` is true for all exit codes >= 1.
456             # See https://stackoverflow.com/a/8254331 for an explanation of the `SETLOCAL` / `ENDLOCAL` mechanics.
457 0           @bins = map { my ($basename, undef, undef) = my_fileparse($_); $basename } @bins;
  0            
  0            
458 0           @bins = uniq(@bins);
459 0           for (@bins) {
460 0           spurt(catfile($shim_dir, $_.'.bat'), <
461             \@ECHO OFF
462             SETLOCAL
463             SET brew_cmd="$brew_exec" internal_win_run \%~n0
464             FOR /F "delims=" \%\%i IN ('\%brew_cmd\%') DO SET command=\%\%i
465             IF ERRORLEVEL 1 EXIT /B \%errorlevel\%
466             ENDLOCAL & "\%command\%" \%*
467             EOT
468             }
469             }
470             else {
471 0           for (@bins) {
472 0           symlink $0, catfile($shim_dir, $_);
473             }
474             }
475             }