chiark / gitweb /
OOT FIXES
[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
79 our $cargo_lock_update;
80 our $cargo_manifest_args;
81
82 our @configs;
83 our $verbose=1;
84 our ($noact,$dump);
85
86 sub read_or_enoent ($) {
87   my ($fn) = @_;
88   if (!open R, '<', $fn) {
89     return undef if $!==ENOENT;
90     die "$self: open $fn: $!\n";
91   }
92   local ($/) = undef;
93   my ($r) = <R> // die "$self: read $fn: $!\n";
94   $r;
95 }
96
97 sub toml_or_enoent ($$) {
98   my ($f,$what) = @_;
99   my $toml = read_or_enoent($f) // return;
100   my ($v,$e) = from_toml($toml);
101   if (!defined $v) {
102     chomp $e;
103     die "$self: parse TOML: $what: $f: $e\n";
104   }
105   die "$e ?" if length $e;
106   $v;
107 }
108
109 sub load1config ($) {
110   my ($f) = @_;
111   my $toml = toml_or_enoent($f, "config file");
112   push @configs, $toml if defined $toml;
113 }
114
115 sub loadconfigs () {
116   my $cfgleaf = ".nailing-cargo-cfg.toml";
117   load1config("/etc/nailing-cargo/cfg.toml");
118   load1config("$worksphere/$cfgleaf");
119   load1config("$ENV{HOME}/$cfgleaf") if defined $ENV{HOME};
120 }
121
122 sub getcfg ($$) {
123   my ($k, $def) = @_;
124   foreach my $cfg (@configs) {
125     my $v = $cfg->{$k};
126     return $v if defined $v;
127   }
128   return $def;
129 }
130
131 sub unlink_or_enoent ($) { unlink $_[0] or $!==ENOENT; }
132
133 sub takelock () {
134   for (;;) {
135     open LOCK, ">", $lockfile or die "$self: open/create $lockfile: $!\n";
136     flock LOCK, LOCK_EX or die "$self: lock $lockfile: $!\n";
137     my @fstat = stat LOCK or die "$self: fstat: $!\n";
138     my @stat  = stat $lockfile;
139     if (!@stat) {
140       next if $! == ENOENT;
141       die "$self: stat $lockfile: $!\n";
142     }
143     last if "@fstat[0..5]" eq "@stat[0..5]";
144   }
145 }
146 sub unlock () {
147   unlink $lockfile or die "$self: removing lockfile: $!\n";
148 }
149
150 our $nail;
151
152 sub badcfg {
153   my $m = pop @_;
154   $" = '.';
155   die "$self: config key \`@_': $m\n";
156 }
157
158 sub cfg_uc {
159   my $v = $nail;
160   foreach my $k (@_) {
161     last unless defined $v;
162     ref($v) eq 'HASH' or badcfg @_, "parent key \`$k' is not a hash";
163     $v = $v->{$k};
164   }
165   return $v;
166 }
167
168 sub cfg {
169   my $exp = shift @_;
170   my $v = cfg_uc @_;
171   my $got = ref($v) || 'scalar';
172   return $v if !defined($v) || $got eq $exp;
173   badcfg @_, "found \L$got\E, expected \L$exp\E";
174   # ^ toml doesn't make refs to scalars, so this is unambiguous
175 }
176
177 sub cfgn {
178   my $exp = shift @_;
179   (cfg $exp, @_) // badcfg @_, "missing";
180 }
181
182 sub cfgs  { cfg  'scalar', @_ }
183 sub cfgsn { cfgn 'scalar', @_ }
184
185 sub cfgn_list {
186   my $l = cfg 'ARRAY', @_;
187   foreach my $x (@$l) {
188     !ref $x or badcfg @_, "list contains non-scalar element";
189   }
190   @$l
191 }
192
193 sub readnail () {
194   my $nailfile = "../Cargo.nail";
195   open N, '<', $nailfile or die "$self: open $nailfile: $!\n";
196   local ($/) = undef;
197   my $toml = <N> // die "$self: read $nailfile: $!";
198   my $transformed;
199   if ($toml !~ m{^\s*\[/}m &&
200       $toml !~ m{^[^\n\#]*\=}m &&
201       # old non-toml syntax
202       $toml =~ s{^[ \t]*([-_0-9a-z]+)[ \t]+(\S+)[ \t]*$}{$1 = \"$2\"}mig) {
203     $toml =~ s{^}{[packages\]\n};
204     my @sd;
205     $toml =~ s{^[ \t]*\-[ \t]*\=[ \t]*(\"[-_0-9a-z]+\"\n?)$}{
206       push @sd, $1; '';
207     }mige;
208     $toml = "subdirs = [\n".(join '', map { "$_\n" } @sd)."]\n".$toml;
209     $transformed = 1;
210   }
211   my $e;
212   ($nail,$e) = from_toml($toml);
213   if (!defined $nail) {
214     if ($transformed) {
215       $toml =~ s/^/    /mg;
216       print STDERR "$self: $nailfile transformed into TOML:\n$toml\n";
217     }
218     $/="\n"; chomp $e;
219     die "$self: parse $nailfile: $e\n";
220   }
221   die "$e ?" if length $e;
222
223   if (!ref $nail->{subdirs}) {
224     $nail->{subdirs} = [
225       grep /^[^\#]/,
226       map { s/^\s+//; s/\s+$//; $_; }
227       split m{\n},
228       $nail->{subdirs}
229     ];
230   }
231 }
232
233 our $oot_dir;      # oot.dir or "Build"
234
235 sub consider_oot () {
236   $oot_dir = cfgs qw(oot dir);
237   my $use = cfgs qw(oot use);
238   unless (defined($oot_dir) || defined($use)) {
239     $cargo_lock_update//=0;
240     return;
241   }
242   $oot_dir //= 'Build';
243 }
244
245 our %manifests;
246 our %packagemap;
247
248 sub read_manifest ($) {
249   my ($subdir) = @_;
250   my $manifest = "../$subdir/Cargo.toml";
251   print STDERR "$self: reading $manifest...\n" if $verbose>=4;
252   if (defined $manifests{$manifest}) {
253     print STDERR
254  "$self: warning: $subdir: specified more than once!\n";
255     return undef;
256   }
257   foreach my $try ("$manifest.unnailed", "$manifest") {
258     my $toml = toml_or_enoent($try, "package manifest") // next;
259     my $p = $toml->{package}{name};
260     if (!defined $p) {
261       print STDERR
262  "$self: warning: $subdir: missing package.name in $try, ignoring\n";
263       next;
264     }
265     $manifests{$manifest} = $toml;
266     return $p;
267   }
268   return undef;
269 }
270
271 sub readorigs () {
272   foreach my $p (keys %{ $nail->{packages} }) {
273     my $v = $nail->{packages}{$p};
274     my $subdir = ref($v) ? $v->{subdir} : $v;
275     my $gotpackage = read_manifest($subdir) // '<nothing!>';
276     if ($gotpackage ne $p) {
277       print STDERR
278  "$self: warning: honouring Cargo.nail packages.$subdir=$p even though $subdir contains package $gotpackage!\n";
279     }
280     die if defined $packagemap{$p};
281     $packagemap{$p} = $subdir;
282   }
283   foreach my $subdir (@{ $nail->{subdirs} }) {
284     my $gotpackage = read_manifest($subdir);
285     if (!defined $gotpackage) {
286       print STDERR
287  "$self: warning: ignoring subdir $subdir which has no Cargo.toml\n";
288       next;
289     }
290     $packagemap{$gotpackage} //= $subdir;
291   }
292 }
293
294 sub calculate () {
295   foreach my $p (sort keys %packagemap) {
296     print STDERR "$self: package $p in $packagemap{$p}\n" if $verbose>=2;
297   }
298   foreach my $mf (keys %manifests) {
299     my $toml = $manifests{$mf};
300     foreach my $k (qw(dependencies build-dependencies dev-dependencies)) {
301       my $deps = $toml->{$k};
302       next unless $deps;
303       foreach my $p (keys %packagemap) {
304         my $info = $deps->{$p};
305         next unless defined $info;
306         $deps->{$p} = $info = { } unless ref $info;
307         delete $info->{version};
308         $info->{path} = $worksphere.'/'.$packagemap{$p};
309       }
310     }
311     my $nailing = "$mf.nailing~";
312     unlink_or_enoent $nailing or die "$self: remove old $nailing: $!\n";
313     open N, '>', $nailing or die "$self: create new $nailing: $!\n";
314     print N to_toml($toml) or die "$self: write new $nailing: $!\n";
315     close N or die "$self: close new $nailing: $!\n";
316   }
317 }
318
319 sub addargs () {
320   if (@ARGV>=2 &&
321       $ARGV[0] =~ m{\bcargo\b} &&
322       $ARGV[1] =~ m/generate-lockfile|update/) {
323     $cargo_lock_update //= 1;
324   } else {
325     $cargo_lock_update //= 0;
326   }
327   $cargo_manifest_args //=
328     (defined $oot_dir) && !$cargo_lock_update;
329
330   if ($cargo_manifest_args) {
331     push @ARGV, "--manifest-path=${src_absdir}/Cargo.toml",
332       qw(--locked --target-dir=target);
333   }
334
335
336 our $oot_absdir;
337 our $build_absdir; # .../Build/<subdir>
338
339 sub oot_massage_cmdline () {
340   return unless defined $oot_dir;
341
342   my $use = cfgs qw(oot use);
343   $oot_absdir = ($oot_dir !~ m{^/} ? "$worksphere/" : ""). $oot_dir;
344   $build_absdir = "$oot_absdir/$subdir";
345
346   my ($pre,$post);
347   my @xargs;
348   if (!$cargo_lock_update) {
349     push @xargs, $build_absdir;
350     ($pre, $post) = ('cd "$1"; shift;', '');
351   } else {
352     push @xargs, $oot_absdir, $subdir, $src_absdir;
353     ($pre, $post) = (<<'END', <<'END');
354         cd "$1"; shift;
355         mkdir -p -- "$1"; cd "$1"; shift;
356         cp -- "$1"/Cargo.toml "$1"/Cargo.lock .; shift;
357         mkdir -p src; >src/lib.rs;
358 END
359         rm -r src Cargo.toml;
360 END
361     $pre  =~ s/^\s+//mg; $pre  =~ s/^\s+\n/ /g;
362     $post =~ s/^\s+//mg; $post =~ s/^\s+\n/ /g;
363   }
364   my $addpath = (cfg qw(oot path_add)) //
365     $use eq 'really' ? Types::Serialiser::true : Types::Serialiser::false;
366   $addpath =
367     !Types::Serialiser::is_bool $addpath ? $addpath           :
368     $addpath                             ? '$HOME/.cargo/bin' :
369                                            undef;
370   if (defined $addpath) {
371     $pre .= <<END
372         PATH=$addpath:\${PATH-/usr/local/bin:/bin:/usr/bin};
373         export PATH;
374 END
375   }
376
377   my $getuser = sub { cfgsn qw(oot user) };
378   my @command;
379   my $xe = $verbose >= 2 ? 'xe' : 'e';
380   my $sh_ec = sub {
381     if (!length $post) {
382       @command = (@_, 'sh',"-${xe}c",$pre.' exec "$@"','--',@xargs);
383     } else {
384       @command = (@_, 'sh',"-${xe}c",$pre.' "$@"; '.$post,'--',@xargs);
385     }
386     push @command, @ARGV;
387   };
388   my $command_sh = sub {
389     my $quoted = join ' ', map {
390       return $_ if !m/\W/;
391       s/\'/\'\\'\'/g;
392       "'$_'"
393     } @ARGV;
394     @command = @_, "set -${xe}; $pre $quoted; $post";
395   };
396   print STDERR "$self: out-of-tree, building in: \`$build_absdir'\n"
397     if $verbose;
398   if ($use eq 'really') {
399     my $user = $getuser->();
400     my @pw = getpwnam $user or die "$self: oot.user \`$user' lookup failed\n";
401     my $homedir = $pw[7];
402     $sh_ec->('really','-u',$user,'env',"HOME=$homedir");
403     print STDERR "$self: using really to run as user \`$user'\n" if $verbose;
404   } elsif ($use eq 'ssh') {
405     my $user = $getuser->();
406     $user .= '@localhost' unless $user =~ m/\@/;
407     $command_sh->('ssh',$user);
408     print STDERR "$self: using ssh to run as \`$user'\n" if $verbose;
409   } elsif ($use eq 'command_args') {
410     my @c = cfgn_list qw(oot command);
411     $sh_ec->(@c);
412     print STDERR "$self: out-of-tree, adverbial command: @c\n" if $verbose;
413   } elsif ($use eq 'command_sh') {
414     my @c = cfgn_list qw(oot command);
415     $command_sh->(@c);
416     print STDERR "$self: out-of-tree, ssh'ish command: @c\n" if $verbose;
417   } else {
418     die "$self: oot.use mode $use not recognised\n";
419   }
420   die unless @command;
421   @ARGV = @command;
422 }
423
424 our $want_uninstall;
425
426 END {
427   if ($want_uninstall) {
428     local ($?);
429     foreach my $mf (keys %manifests) {
430       eval { uninstall1($mf,1); 1; } or warn "$@";
431     }
432   }
433 }
434
435 sub makebackups () {
436   foreach my $mf (keys %manifests) {
437     link "$mf", "$mf.unnailed" or $!==EEXIST
438       or die "$self: make backup link $mf.unnailed: $!\n";
439   }
440 }
441
442 sub nailed ($) {
443   my ($mf) = @_;
444   my $nailed  = "$mf.nailed~"; $nailed =~ s{/([^/]+)$}{/.$1} or die;
445   $nailed;
446 }    
447
448 sub install () {
449   foreach my $mf (keys %manifests) {
450     my $nailing = "$mf.nailing~";
451     my $nailed = nailed($mf);
452     my ($use, $rm);
453     my $diff;
454     if (open NN, '<', $nailed) {
455       $diff = compare($nailing, \*NN);
456       die "$self: compare $nailing and $nailed: $!" if $diff<0;
457     } else {
458       $!==ENOENT or die "$self: check previous $nailed: $!\n";
459       $diff = 1;
460     }
461     if ($diff) {
462       $use = $nailing;
463       $rm  = $nailed;
464     } else {
465       $use = $nailed;
466       $rm  = $nailing;
467     }
468     rename $use, $mf or die "$self: install nailed $use: $!\n";
469     unlink_or_enoent $rm or die "$self: remove old $rm: $!\n";
470     print STDERR "$self: nailed $mf\n" if $verbose>=3;
471   }
472 }
473
474 sub invoke () {
475   my $r = system @ARGV;
476   if (!$r) {
477     return 0;
478   } elsif ($r<0) {
479     print STDERR "$self: could not execute $ARGV[0]: $!\n";
480     return 127;
481   } elsif ($r & 0xff00) {
482     print STDERR "$self: $ARGV[0] failed (exit status $r)\n";
483     return $r >> 8;
484   } else {
485     print STDERR "$self: $ARGV[0] died due to signal! (wait status $r)\n";
486     return 125;
487   }
488 }
489
490 sub cargo_lock_update_after () {
491   return unless $cargo_lock_update;
492   # avoids importing File::Copy and the error handling is about as good
493   $!=0; $?=0;
494   my $r= system qw(cp --), "$build_absdir/Cargo.lock", "Cargo.lock";
495   die "$self: run cp: $! $?" if $r<0 || $r & 0xff;
496   die "$self: failed to update local Cargo.lock (wait status $r)\n" if $r;
497 }
498
499 sub uninstall1 ($$) {
500   my ($mf, $enoentok) = @_;
501   my $unnailed = "$mf.unnailed";
502   rename $unnailed, $mf or ($enoentok && $!==ENOENT)
503     or die "$self: failed to restore: rename $unnailed back to $mf: $!\n";
504 }
505
506 sub uninstall () {
507   foreach my $mf (keys %manifests) {
508     my $nailed = nailed($mf);
509     link $mf, $nailed or die "$self: preserve (link) $mf as $nailed: $!\n";
510     uninstall1($mf,0);
511   }
512 }
513
514 while (@ARGV && $ARGV[0] =~ m/^-/) {
515   $_ = shift @ARGV;
516   last if m{^--$};
517   if (m{^-[^-]}) {
518     while (m{^-.}) {
519       if (s{^-v}{-}) {
520         $verbose++;
521       } elsif (s{^-q}{-}) {
522         $verbose=0;
523       } elsif (s{^-n}{-}) {
524         $noact++;
525       } elsif (s{^-D}{-}) {
526         $dump++;
527       } elsif (s{^-([uU])}{-}) {
528         $cargo_lock_update= $1=~m/[A-Z]/;
529       } elsif (s{^-([mM])}{-}) {
530         $cargo_manifest_args= $1=~m/[A-Z]/;
531       } else {
532         die "$self: unknown short option(s) $_\n";
533       }
534     }
535   } elsif (m{^--no-cargo-lock-update}) {
536     $cargo_lock_update=0;
537   } else {
538     die "$self: unknown long option $_\n";
539   }
540 }
541
542 die "$self: need command to run\n" unless @ARGV || $noact;
543
544 takelock();
545 readnail();
546 consider_oot();
547 readorigs();
548 calculate();
549 addargs();
550 oot_massage_cmdline();
551 setenvs();
552
553 if ($dump) {
554   eval '
555     use Data::Dumper;
556     print STDERR Dumper(\%manifests) if $dump>=2;
557     print STDERR Dumper(\%packagemap, \@ARGV,
558                         { src_absdir => $src_absdir,
559                           worksphere => $worksphere,
560                           subdir => $subdir,
561                           oot_dir => $oot_dir,
562                           oot_absdir => $oot_absdir,
563                           build_absdir => $build_absdir });
564   ' or die $@;
565 }
566
567 exit 0 if $noact;
568
569 $want_uninstall = 1;
570 makebackups();
571 install();
572
573 printf STDERR "$self: nailed (%s manifests, %s packages)\n",
574   (scalar keys %manifests), (scalar keys %packagemap)
575   if $verbose;
576
577 my $estatus = invoke();
578
579 uninstall();
580 $want_uninstall = 1;
581
582 cargo_lock_update_after();
583
584 print STDERR "$self: unnailed.  status $estatus.\n" if $verbose;
585
586 exit $estatus;