chiark / gitweb /
nailing-cargo: wip, bugfixes
[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   if ($use eq 'really') {
376     my $user = $getuser->();
377     my @pw = getpwnam $user or die "$self: oot.user \`$user' lookup failed\n";
378     my $homedir = $pw[7];
379     $sh_ec->('really','-u',$user,'env',"HOME=$homedir");
380   } elsif ($use eq 'ssh') {
381     my $user = $getuser->();
382     $user .= '@localhost' unless $user =~ m/\@/;
383     $command_sh->('ssh',$user);
384   } elsif ($use eq 'command_sh') {
385     $command_sh->(cfgn_list qw(oot command));
386   } elsif ($use eq 'command_args') {
387     $sh_ec->(cfgn_list qw(oot command))
388   } else {
389     die "$self: oot.use mode $use not recognised\n";
390   }
391   die unless @command;
392   @ARGV = @command;
393 }
394
395 our $want_uninstall;
396
397 END {
398   if ($want_uninstall) {
399     local ($?);
400     foreach my $mf (keys %manifests) {
401       eval { uninstall1($mf,1); 1; } or warn "$@";
402     }
403   }
404 }
405
406 sub makebackups () {
407   foreach my $mf (keys %manifests) {
408     link "$mf", "$mf.unnailed" or $!==EEXIST
409       or die "$self: make backup link $mf.unnailed: $!\n";
410   }
411 }
412
413 sub nailed ($) {
414   my ($mf) = @_;
415   my $nailed  = "$mf.nailed~"; $nailed =~ s{/([^/]+)$}{/.$1} or die;
416   $nailed;
417 }    
418
419 sub install () {
420   foreach my $mf (keys %manifests) {
421     my $nailing = "$mf.nailing~";
422     my $nailed = nailed($mf);
423     my ($use, $rm);
424     my $diff;
425     if (open NN, '<', $nailed) {
426       $diff = compare($nailing, \*NN);
427       die "$self: compare $nailing and $nailed: $!" if $diff<0;
428     } else {
429       $!==ENOENT or die "$self: check previous $nailed: $!\n";
430       $diff = 1;
431     }
432     if ($diff) {
433       $use = $nailing;
434       $rm  = $nailed;
435     } else {
436       $use = $nailed;
437       $rm  = $nailing;
438     }
439     rename $use, $mf or die "$self: install nailed $use: $!\n";
440     unlink_or_enoent $rm or die "$self: remove old $rm: $!\n";
441     print STDERR "$self: nailed $mf\n" if $verbose>=3;
442   }
443 }
444
445 sub invoke () {
446   my $r = system @ARGV;
447   if (!$r) {
448     return 0;
449   } elsif ($r<0) {
450     print STDERR "$self: could not execute $ARGV[0]: $!\n";
451     return 127;
452   } elsif ($r & 0xff00) {
453     print STDERR "$self: $ARGV[0] failed (exit status $r)\n";
454     return $r >> 8;
455   } else {
456     print STDERR "$self: $ARGV[0] died due to signal! (wait status $r)\n";
457     return 125;
458   }
459 }
460
461 sub uninstall1 ($$) {
462   my ($mf, $enoentok) = @_;
463   my $unnailed = "$mf.unnailed";
464   rename $unnailed, $mf or ($enoentok && $!==ENOENT)
465     or die "$self: failed to restore: rename $unnailed back to $mf: $!\n";
466 }
467
468 sub uninstall () {
469   foreach my $mf (keys %manifests) {
470     my $nailed = nailed($mf);
471     link $mf, $nailed or die "$self: preserve (link) $mf as $nailed: $!\n";
472     uninstall1($mf,0);
473   }
474 }
475
476 while (@ARGV && $ARGV[0] =~ m/^-/) {
477   $_ = shift @ARGV;
478   last if m{^--$};
479   if (m{^-[^-]}) {
480     while (m{^-.}) {
481       if (s{^-v}{-}) {
482         $verbose++;
483       } elsif (s{^-q}{-}) {
484         $verbose=0;
485       } elsif (s{^-n}{-}) {
486         $noact++;
487       } elsif (s{^-D}{-}) {
488         $dump++;
489       } elsif (s{^-L}{-}) {
490         $oot_cargo_lock_faff=1;
491       } else {
492         die "$self: unknown short option(s) $_\n";
493       }
494     }
495   } else {
496     die "$self: unknown long option $_\n";
497   }
498 }
499
500 die "$self: need command to run\n" unless @ARGV || $noact;
501
502 takelock();
503 readnail();
504 readorigs();
505 calculate();
506 calculate_oot();
507
508 if ($dump) {
509   eval '
510     use Data::Dumper;
511     print STDERR Dumper(\%manifests, \%packagemap, \@ARGV);
512   ' or die $@;
513 }
514
515 exit 0 if $noact;
516
517 $want_uninstall = 1;
518 makebackups();
519 install();
520
521 printf STDERR "$self: Nailed (%s manifests, %s packages)\n",
522   (scalar keys %manifests), (scalar keys %packagemap)
523   if $verbose;
524
525 my $estatus = invoke();
526
527 uninstall();
528 $want_uninstall = 1;
529
530 get_cargo_lock() if $oot_cargo_lock_faff;
531
532 print STDERR "$self: unnailed.  status $estatus.\n" if $verbose;
533
534 exit $estatus;