chiark / gitweb /
nailing-cargo: message and debug improvements
[nailing-cargo.git] / nailing-cargo
1 #!/usr/bin/perl -w
2
3 #    nailing-cargo: wrapper to use unpublished local crates
4 #
5 #    Copyright (C) 2019-2020 Ian Jackson
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 # example usages:
21 #   ../nailing-cargo/nailing-caretwgo make
22 #   ../nailing-cargo/nailing-cargo cargo build
23 #   CARGO='../nailing-cargo/nailing-cargo cargo' make
24
25 # Why do we need this ?
26 #
27 #  https://github.com/rust-lang/cargo/issues/6713
28 #  https://stackoverflow.com/questions/33025887/how-to-use-a-local-unpublished-crate
29 #  https://github.com/rust-lang/cargo/issues/1481
30
31 # Needs libtoml-perl
32
33 #: Cargo.nail:
34 #
35 #    [packages]
36 #    package = subdir
37 #    package = { subdir = ... }
38 #
39 #    [subdirs]
40 #    subdir
41
42 our $self;
43
44 use strict;
45 use POSIX;
46 use Types::Serialiser;
47
48 BEGIN {
49   $self = $0;  $self =~ s{^.*/(?=.)}{};
50   my $deref = $0;
51   while ($deref =~ m{^/}) {
52     my $link = readlink $deref;
53     if (!defined $link) {
54       $! == EINVAL
55         or die "$self: checking our script location $deref: $!\n";
56       $deref =~ s{/[^/]+$}{}
57         or die "$self: unexpected script path: $deref\n";
58       unshift @INC, $deref."/TOML-Tiny/lib";
59       last;
60     }
61     last if $link !~ m{^/};
62     $deref = $link;
63   }
64 }
65
66 use Fcntl qw(LOCK_EX);
67 use File::Compare;
68 use TOML::Tiny::Faithful;
69
70 our $src_absdir = getcwd() // die "$self: getcwd failed: $!\n";
71
72 our $worksphere = $src_absdir;
73 $worksphere =~ s{/([^/]+)$}{}
74   or die "$self: cwd \`$worksphere' unsupported!\n";
75 our $subdir = $1; # leafname
76
77 our $lockfile = "../.nailing-cargo.lock";
78 our $oot_cargo_lock_faff;
79
80 our @configs;
81 our $verbose=1;
82 our ($noact,$dump);
83
84 sub read_or_enoent ($) {
85   my ($fn) = @_;
86   if (!open R, '<', $fn) {
87     return undef if $!==ENOENT;
88     die "$self: open $fn: $!\n";
89   }
90   local ($/) = undef;
91   my ($r) = <R> // die "$self: read $fn: $!\n";
92   $r;
93 }
94
95 sub toml_or_enoent ($$) {
96   my ($f,$what) = @_;
97   my $toml = read_or_enoent($f) // return;
98   my ($v,$e) = from_toml($toml);
99   if (!defined $v) {
100     chomp $e;
101     die "$self: parse TOML: $what: $f: $e\n";
102   }
103   die "$e ?" if length $e;
104   $v;
105 }
106
107 sub load1config ($) {
108   my ($f) = @_;
109   my $toml = toml_or_enoent($f, "config file");
110   push @configs, $toml if defined $toml;
111 }
112
113 sub loadconfigs () {
114   my $cfgleaf = ".nailing-cargo-cfg.toml";
115   load1config("/etc/nailing-cargo/cfg.toml");
116   load1config("$worksphere/$cfgleaf");
117   load1config("$ENV{HOME}/$cfgleaf") if defined $ENV{HOME};
118 }
119
120 sub getcfg ($$) {
121   my ($k, $def) = @_;
122   foreach my $cfg (@configs) {
123     my $v = $cfg->{$k};
124     return $v if defined $v;
125   }
126   return $def;
127 }
128
129 sub unlink_or_enoent ($) { unlink $_[0] or $!==ENOENT; }
130
131 sub takelock () {
132   for (;;) {
133     open LOCK, ">", $lockfile or die "$self: open/create $lockfile: $!\n";
134     flock LOCK, LOCK_EX or die "$self: lock $lockfile: $!\n";
135     my @fstat = stat LOCK or die "$self: fstat: $!\n";
136     my @stat  = stat $lockfile;
137     if (!@stat) {
138       next if $! == ENOENT;
139       die "$self: stat $lockfile: $!\n";
140     }
141     last if "@fstat[0..5]" eq "@stat[0..5]";
142   }
143 }
144 sub unlock () {
145   unlink $lockfile or die "$self: removing lockfile: $!\n";
146 }
147
148 our $nail;
149
150 sub badcfg {
151   my $m = pop @_;
152   $" = '.';
153   die "$self: config key \`@_': $m\n";
154 }
155
156 sub cfg_uc {
157   my $v = $nail;
158   foreach my $k (@_) {
159     last unless defined $v;
160     ref($v) eq 'HASH' or badcfg @_, "parent key \`$k' is not a hash";
161     $v = $v->{$k};
162   }
163   return $v;
164 }
165
166 sub cfg {
167   my $exp = shift @_;
168   my $v = cfg_uc @_;
169   my $got = ref($v) || 'scalar';
170   return $v if !defined($v) || $got eq $exp;
171   badcfg @_, "found \L$got\E, expected \L$exp\E";
172   # ^ toml doesn't make refs to scalars, so this is unambiguous
173 }
174
175 sub cfgn {
176   my $exp = shift @_;
177   (cfg $exp, @_) // badcfg @_, "missing";
178 }
179
180 sub cfgs  { cfg  'scalar', @_ }
181 sub cfgsn { cfgn 'scalar', @_ }
182
183 sub cfgn_list {
184   my $l = cfg 'ARRAY', @_;
185   foreach my $x (@$l) {
186     !ref $x or badcfg @_, "list contains non-scalar element";
187   }
188   @$l
189 }
190
191 sub readnail () {
192   my $nailfile = "../Cargo.nail";
193   open N, '<', $nailfile or die "$self: open $nailfile: $!\n";
194   local ($/) = undef;
195   my $toml = <N> // die "$self: read $nailfile: $!";
196   my $transformed;
197   if ($toml !~ m{^\s*\[/}m &&
198       $toml !~ m{^[^\n\#]*\=}m &&
199       # old non-toml syntax
200       $toml =~ s{^[ \t]*([-_0-9a-z]+)[ \t]+(\S+)[ \t]*$}{$1 = \"$2\"}mig) {
201     $toml =~ s{^}{[packages\]\n};
202     my @sd;
203     $toml =~ s{^[ \t]*\-[ \t]*\=[ \t]*(\"[-_0-9a-z]+\"\n?)$}{
204       push @sd, $1; '';
205     }mige;
206     $toml = "subdirs = [\n".(join '', map { "$_\n" } @sd)."]\n".$toml;
207     $transformed = 1;
208   }
209   my $e;
210   ($nail,$e) = from_toml($toml);
211   if (!defined $nail) {
212     if ($transformed) {
213       $toml =~ s/^/    /mg;
214       print STDERR "$self: $nailfile transformed into TOML:\n$toml\n";
215     }
216     $/="\n"; chomp $e;
217     die "$self: parse $nailfile: $e\n";
218   }
219   die "$e ?" if length $e;
220
221   if (!ref $nail->{subdirs}) {
222     $nail->{subdirs} = [
223       grep /^[^\#]/,
224       map { s/^\s+//; s/\s+$//; $_; }
225       split m{\n},
226       $nail->{subdirs}
227     ];
228   }
229 }
230
231 our %manifests;
232 our %packagemap;
233
234 sub read_manifest ($) {
235   my ($subdir) = @_;
236   my $manifest = "../$subdir/Cargo.toml";
237   print STDERR "$self: reading $manifest...\n" if $verbose>=4;
238   if (defined $manifests{$manifest}) {
239     print STDERR
240  "$self: warning: $subdir: specified more than once!\n";
241     return undef;
242   }
243   foreach my $try ("$manifest.unnailed", "$manifest") {
244     my $toml = toml_or_enoent($try, "package manifest") // next;
245     my $p = $toml->{package}{name};
246     if (!defined $p) {
247       print STDERR
248  "$self: warning: $subdir: missing package.name in $try, ignoring\n";
249       next;
250     }
251     $manifests{$manifest} = $toml;
252     return $p;
253   }
254   return undef;
255 }
256
257 sub readorigs () {
258   foreach my $p (keys %{ $nail->{packages} }) {
259     my $v = $nail->{packages}{$p};
260     my $subdir = ref($v) ? $v->{subdir} : $v;
261     my $gotpackage = read_manifest($subdir) // '<nothing!>';
262     if ($gotpackage ne $p) {
263       print STDERR
264  "$self: warning: honouring Cargo.nail packages.$subdir=$p even though $subdir contains package $gotpackage!\n";
265     }
266     die if defined $packagemap{$p};
267     $packagemap{$p} = $subdir;
268   }
269   foreach my $subdir (@{ $nail->{subdirs} }) {
270     my $gotpackage = read_manifest($subdir);
271     if (!defined $gotpackage) {
272       print STDERR
273  "$self: warning: ignoring subdir $subdir which has no Cargo.toml\n";
274       next;
275     }
276     $packagemap{$gotpackage} //= $subdir;
277   }
278 }
279
280 sub calculate () {
281   foreach my $p (sort keys %packagemap) {
282     print STDERR "$self: package $p in $packagemap{$p}\n" if $verbose>=2;
283   }
284   foreach my $mf (keys %manifests) {
285     my $toml = $manifests{$mf};
286     foreach my $k (qw(dependencies build-dependencies dev-dependencies)) {
287       my $deps = $toml->{$k};
288       next unless $deps;
289       foreach my $p (keys %packagemap) {
290         my $info = $deps->{$p};
291         next unless defined $info;
292         $deps->{$p} = $info = { } unless ref $info;
293         delete $info->{version};
294         $info->{path} = $worksphere.'/'.$packagemap{$p};
295       }
296     }
297     my $nailing = "$mf.nailing~";
298     unlink_or_enoent $nailing or die "$self: remove old $nailing: $!\n";
299     open N, '>', $nailing or die "$self: create new $nailing: $!\n";
300     print N to_toml($toml) or die "$self: write new $nailing: $!\n";
301     close N or die "$self: close new $nailing: $!\n";
302   }
303 }
304
305 our @out_command;
306
307 our $oot_dir;      # oot.dir or "Build"
308 our $oot_absdir;
309
310 our $build_absdir; # .../Build/<subdir>
311
312 sub calculate_oot () {
313   $oot_dir = cfgs qw(oot dir);
314   my $use = cfgs qw(oot use);
315   return unless defined($oot_dir) || defined($use);
316   $oot_dir //= 'Build';
317
318   if (@ARGV && $ARGV[0] =~ m/generate-lockfile|update/) {
319     $oot_cargo_lock_faff = 1;
320   }
321
322   $oot_absdir = ($oot_dir !~ m{^/} ? "$worksphere/" : ""). $oot_dir;
323   $build_absdir = "$oot_absdir/$subdir";
324
325   my ($pre,$post);
326   my @xargs;
327   if (!$oot_cargo_lock_faff) {
328     push @xargs, $build_absdir;
329     ($pre, $post) = ('cd "$1"; shift;', '');
330   } else {
331     push @xargs, $build_absdir, $subdir, $src_absdir;
332     ($pre, $post) = (<<'END', <<'END');
333         cd "$1"; shift;
334         mkdir -p -- "$1"; cd "$1"; shift;
335         cp -- "$1"/Cargo.toml "$1"/Cargo.lock .; shift;
336         mkdir -p src; >src/lib.rs;
337 END
338         rm -r src Cargo.toml;
339 END
340     $pre  =~ s/^\s+//mg; $pre  =~ s/^\s+\n/ /g;
341     $post =~ s/^\s+//mg; $post =~ s/^\s+\n/ /g;
342   }
343   my $addpath = (cfg qw(oot path_add)) //
344     $use eq 'really' ? Types::Serialiser::true : Types::Serialiser::false;
345   $addpath =
346     !Types::Serialiser::is_bool $addpath ? $addpath           :
347     $addpath                             ? '$HOME/.cargo/bin' :
348                                            undef;
349   if (defined $addpath) {
350     $pre .= <<END
351         PATH=$addpath:\${PATH-/usr/local/bin:/bin:/usr/bin};
352         export PATH;
353 END
354   }
355
356   my $getuser = sub { cfgsn qw(oot user) };
357   my @command;
358   my $xe = $verbose >= 2 ? 'xe' : 'e';
359   my $sh_ec = sub {
360     if (!length $post) {
361       @command = (@_, 'sh',"-${xe}c",$pre.' exec "$@"','--',@xargs);
362     } else {
363       @command = (@_, 'sh',"-${xe}c",$pre.' "$@"; '.$post,'--',@xargs);
364     }
365     push @command, @ARGV;
366   };
367   my $command_sh = sub {
368     my $quoted = join ' ', map {
369       return $_ if !m/\W/;
370       s/\'/\'\\'\'/g;
371       "'$_'"
372     } @ARGV;
373     @command = @_, "set -${xe}; $pre $quoted; $post";
374   };
375   print STDERR "$self: out-of-tree, building in: \`$build_absdir'\n"
376     if $verbose;
377   if ($use eq 'really') {
378     my $user = $getuser->();
379     my @pw = getpwnam $user or die "$self: oot.user \`$user' lookup failed\n";
380     my $homedir = $pw[7];
381     $sh_ec->('really','-u',$user,'env',"HOME=$homedir");
382     print STDERR "$self: using really to run as user \`$user'\n" if $verbose;
383   } elsif ($use eq 'ssh') {
384     my $user = $getuser->();
385     $user .= '@localhost' unless $user =~ m/\@/;
386     $command_sh->('ssh',$user);
387   } elsif ($use eq 'command_sh') {
388     $command_sh->(cfgn_list qw(oot command));
389   } elsif ($use eq 'command_args') {
390     $sh_ec->(cfgn_list qw(oot command))
391   } else {
392     die "$self: oot.use mode $use not recognised\n";
393   }
394   die unless @command;
395   @ARGV = @command;
396 }
397
398 our $want_uninstall;
399
400 END {
401   if ($want_uninstall) {
402     local ($?);
403     foreach my $mf (keys %manifests) {
404       eval { uninstall1($mf,1); 1; } or warn "$@";
405     }
406   }
407 }
408
409 sub makebackups () {
410   foreach my $mf (keys %manifests) {
411     link "$mf", "$mf.unnailed" or $!==EEXIST
412       or die "$self: make backup link $mf.unnailed: $!\n";
413   }
414 }
415
416 sub nailed ($) {
417   my ($mf) = @_;
418   my $nailed  = "$mf.nailed~"; $nailed =~ s{/([^/]+)$}{/.$1} or die;
419   $nailed;
420 }    
421
422 sub install () {
423   foreach my $mf (keys %manifests) {
424     my $nailing = "$mf.nailing~";
425     my $nailed = nailed($mf);
426     my ($use, $rm);
427     my $diff;
428     if (open NN, '<', $nailed) {
429       $diff = compare($nailing, \*NN);
430       die "$self: compare $nailing and $nailed: $!" if $diff<0;
431     } else {
432       $!==ENOENT or die "$self: check previous $nailed: $!\n";
433       $diff = 1;
434     }
435     if ($diff) {
436       $use = $nailing;
437       $rm  = $nailed;
438     } else {
439       $use = $nailed;
440       $rm  = $nailing;
441     }
442     rename $use, $mf or die "$self: install nailed $use: $!\n";
443     unlink_or_enoent $rm or die "$self: remove old $rm: $!\n";
444     print STDERR "$self: nailed $mf\n" if $verbose>=3;
445   }
446 }
447
448 sub invoke () {
449   my $r = system @ARGV;
450   if (!$r) {
451     return 0;
452   } elsif ($r<0) {
453     print STDERR "$self: could not execute $ARGV[0]: $!\n";
454     return 127;
455   } elsif ($r & 0xff00) {
456     print STDERR "$self: $ARGV[0] failed (exit status $r)\n";
457     return $r >> 8;
458   } else {
459     print STDERR "$self: $ARGV[0] died due to signal! (wait status $r)\n";
460     return 125;
461   }
462 }
463
464 sub uninstall1 ($$) {
465   my ($mf, $enoentok) = @_;
466   my $unnailed = "$mf.unnailed";
467   rename $unnailed, $mf or ($enoentok && $!==ENOENT)
468     or die "$self: failed to restore: rename $unnailed back to $mf: $!\n";
469 }
470
471 sub uninstall () {
472   foreach my $mf (keys %manifests) {
473     my $nailed = nailed($mf);
474     link $mf, $nailed or die "$self: preserve (link) $mf as $nailed: $!\n";
475     uninstall1($mf,0);
476   }
477 }
478
479 while (@ARGV && $ARGV[0] =~ m/^-/) {
480   $_ = shift @ARGV;
481   last if m{^--$};
482   if (m{^-[^-]}) {
483     while (m{^-.}) {
484       if (s{^-v}{-}) {
485         $verbose++;
486       } elsif (s{^-q}{-}) {
487         $verbose=0;
488       } elsif (s{^-n}{-}) {
489         $noact++;
490       } elsif (s{^-D}{-}) {
491         $dump++;
492       } elsif (s{^-L}{-}) {
493         $oot_cargo_lock_faff=1;
494       } else {
495         die "$self: unknown short option(s) $_\n";
496       }
497     }
498   } else {
499     die "$self: unknown long option $_\n";
500   }
501 }
502
503 die "$self: need command to run\n" unless @ARGV || $noact;
504
505 takelock();
506 readnail();
507 readorigs();
508 calculate();
509 calculate_oot();
510
511 if ($dump) {
512   eval '
513     use Data::Dumper;
514     print STDERR Dumper(\%manifests) if $dump>=2;
515     print STDERR Dumper(\%packagemap, \@ARGV,
516                         { src_absdir => $src_absdir,
517                           worksphere => $worksphere,
518                           subdir => $subdir,
519                           oot_dir => $oot_dir,
520                           oot_absdir => $oot_absdir,
521                           build_absdir => $build_absdir });
522   ' or die $@;
523 }
524
525 exit 0 if $noact;
526
527 $want_uninstall = 1;
528 makebackups();
529 install();
530
531 printf STDERR "$self: nailed (%s manifests, %s packages)\n",
532   (scalar keys %manifests), (scalar keys %packagemap)
533   if $verbose;
534
535 print STDERR "$self: invoking: @display_cmd\n" if $verbose;
536 my $estatus = invoke();
537
538 uninstall();
539 $want_uninstall = 1;
540
541 get_cargo_lock() if $oot_cargo_lock_faff;
542
543 print STDERR "$self: unnailed.  status $estatus.\n" if $verbose;
544
545 exit $estatus;