File Coverage

blib/lib/App/Rakubrew/VersionHandling.pm
Criterion Covered Total %
statement 147 273 53.8
branch 46 148 31.0
condition 15 105 14.2
subroutine 29 35 82.8
pod 0 24 0.0
total 237 585 40.5


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