chiark / gitweb /
Clean on publish
[nailing-cargo.git] / nailing-cargo
1 #!/usr/bin/perl -w
2 # nailing-cargo: wrapper to use unpublished local crates
3 # SPDX-License-Identifier: AGPL-3.0-or-later
4 our $usage = <<'END';
5
6 usages:
7
8   nailing-cargo <nailing-opts> <cargo-opts> [--] <subcmd>...
9   nailing-cargo <nailing-opts> --- <cargo> <cargo-opts> [--] <subcmd>...
10   nailing-cargo <nailing-opts> --- [--] <build-command>...
11
12 options:
13
14   -v  Increase verbosity.  (Default is 1)
15   -q  Set verbosity to 0
16   -D  Increase amount of debugging dump.
17   -n  "No action": stop after writing Cargo.toml.nailing~
18       everywhere, and do not run any build command
19
20   -c  Do add cargo command line options      } default is add if
21   -C  Do not add cargo command line options  }  command is cargo
22
23   -o --online                     -O --offline
24   -u --cargo-lock-update          -U --no-cargo-lock-update
25
26   -T<arch>  --target=<arch>       Specify target architecture
27   -h --help                       Print this message
28   --doc --man --manual            Display complete manual (in w3m)
29   --leave-nailed                  Leave the nailed Cargo.toml in place
30   --just-linkfarm | --clean-linkfarm | --keep-linkfarm (default is keep)
31   --[no-]preclean-linkfarm[=no|src|full]               (default is no)
32   --just-run                      Run the command, don't do cargo stuff
33   --no-nail                       Do not nail, just run the command.
34   --no-cargo-lock-manip           Do not manipulate Cargo.lock.
35   --no-concurrency-lock           Do not take the concurrency lock.
36
37   -s<subcommand>                  Treat command as `cargo <subcommand>`
38   --subcommand-props=<prop>,...   Override command props (see docs)
39
40 END
41
42 our $self;
43
44 use strict;
45 use POSIX;
46 use Types::Serialiser;
47 use File::Glob qw(bsd_glob GLOB_ERR GLOB_BRACE GLOB_NOMAGIC);
48 use Cwd qw(realpath);
49
50 our $base_path;
51 our %archmap = (
52     RPI => 'arm-unknown-linux-gnueabihf',
53     WASM => 'wasm32-unknown-unknown',
54 );
55
56 BEGIN {
57   $self = $0;  $self =~ s{^.*/(?=.)}{};
58   my $deref = $0;
59   our $base_path;
60   while ($deref =~ m{^/}) {
61     my $link = readlink $deref;
62     if (!defined $link) {
63       $! == EINVAL
64         or die "$self: checking our script location $deref: $!\n";
65       $deref =~ s{/[^/]+$}{}
66         or die "$self: unexpected script path: $deref\n";
67       $base_path = $deref;
68       unshift @INC, $deref."/TOML-Tiny/lib";
69       last;
70     }
71     last if $link !~ m{^/};
72     $deref = $link;
73   }
74 }
75
76 use Fcntl qw(LOCK_EX);
77 use File::Compare;
78 use TOML::Tiny::Faithful;
79
80 our $src_absdir = getcwd() // die "$self: getcwd failed: $!\n";
81
82 our $worksphere = $src_absdir;
83 $worksphere =~ s{/([^/]+)$}{}
84   or die "$self: cwd \`$worksphere' unsupported!\n";
85 our $subdir = $1; # leafname
86
87 our $lockfile = "../.nailing-cargo.lock";
88
89 our @args_preface;
90 our $cargo_subcmd;
91 our $command_is_cargo;
92 our $alt_cargo_lock;
93 our $cargo_lock_update;
94 our $pass_options;
95 our $online;
96 our $just_linkfarm;
97 our $leave_nailed;
98 our $oot_clean;
99 our $oot_preclean;
100 our $do_nail=1;
101 our $do_cargo_lock=1;
102 our $do_lock=1;
103 our $linkfarm_depth;
104
105 #
106 our %subcmd_props = (
107 # build (default)  =>[qw(                                                )],
108 'generate-lockfile'=>[qw( lock-update !target          !target-dir       )],
109  update            =>[qw( lock-update !target online                     )],
110  fetch             =>[qw(                     online   !target-dir       )],
111  fmt               =>[qw( !locked     !target !offline !target-dir edits )],
112  miri              =>[qw( !locked             !offline  linkfarm-shallow )],
113  publish           =>[qw(                     !offline  linkfarm-pristine )],
114  upgrades          =>[qw( !locked                      !target-dir       )],
115                     );
116
117 our @subcmd_xprops = qw(!manifest-path);
118
119 our @configs;
120 our $verbose=1;
121 our ($noact,$dump);
122 our $target;
123
124 sub print_usage () {
125   print $usage or die $!;
126   exit 0;
127 }
128
129 sub show_manual () {
130   my $manual = ($base_path // '.').'/README.md';
131   stat $manual or die "$self: manual not found at $manual: $!\n";;
132   exec 'sh','-ec', 'pandoc -- "$1" 2>&1 | w3m -T text/html', '--', $manual;
133   die "$self: exec sh failed: $!";
134 }
135
136 sub read_or_enoent ($) {
137   my ($fn) = @_;
138   if (!open R, '<', $fn) {
139     return undef if $!==ENOENT;
140     die "$self: open $fn: $!\n";
141   }
142   local ($/) = undef;
143   my ($r) = <R> // die "$self: read $fn: $!\n";
144   $r;
145 }
146
147 sub stat_exists ($$) {
148   my ($fn, $what) = @_;
149   if (stat $fn) { return 1; }
150   $!==ENOENT or die "$self: stat $what: $fn: $!\n";
151   return 0;
152 }
153
154 sub subcmd_p ($) {
155   print STDERR " subcmd_p ".(join ' ', keys %$cargo_subcmd)."   | @_\n"
156     if $dump;
157   $cargo_subcmd->{$_[0]}
158 }
159
160 sub toml_or_enoent ($$) {
161   my ($f,$what) = @_;
162   my $toml = read_or_enoent($f) // return;
163   print STDERR "Read TOML from $f\n" if $dump;
164   my ($v,$e) = from_toml($toml);
165   if (!defined $v) {
166     chomp $e;
167     die "$self: parse TOML: $what: $f: $e\n";
168   }
169   die "$e ?" if length $e;
170   $v;
171 }
172
173 sub load1config ($) {
174   my ($f) = @_;
175   my $toml = toml_or_enoent($f, "config file");
176   push @configs, $toml if defined $toml;
177 }
178
179 sub loadconfigs () {
180   my $dotfile = ".nailing-cargo.toml";
181   load1config("../Nailing-Cargo.toml");
182   load1config($dotfile);
183   load1config("$ENV{HOME}/$dotfile") if defined $ENV{HOME};
184   load1config("/etc/nailing-cargo/cfg.toml");
185 }
186
187 sub unlink_or_enoent ($) { unlink $_[0] or $!==ENOENT; }
188
189 sub same_file ($$) {
190   my ($x,$y) = @_;
191   "@$x[0..5]" eq "@$y[0..5]";
192 }
193
194 sub takelock () {
195   return unless $do_lock;
196
197   for (;;) {
198     open LOCK, ">", $lockfile or die "$self: open/create $lockfile: $!\n";
199     flock LOCK, LOCK_EX or die "$self: lock $lockfile: $!\n";
200     my @fstat = stat LOCK or die "$self: fstat: $!\n";
201     my @stat  = stat $lockfile;
202     if (!@stat) {
203       next if $! == ENOENT;
204       die "$self: stat $lockfile: $!\n";
205     }
206     last if same_file(\@fstat,\@stat);
207   }
208 }
209 sub unlock () {
210   unlink $lockfile or die "$self: removing lockfile: $!\n";
211 }
212
213 our $nail;
214
215 sub badcfg {
216   my $m = pop @_;
217   $" = '.';
218   die "$self: config key \`@_': $m\n";
219 }
220
221 sub cfg_uc {
222   foreach my $cfg (@configs) {
223     my $v = $cfg;
224     foreach my $k (@_) {
225       last unless defined $v;
226       ref($v) eq 'HASH' or badcfg @_, "parent key \`$k' is not a hash";
227       $v = $v->{$k};
228     }
229     return $v if defined $v;
230   }
231   return undef;
232 }
233
234 sub cfge {
235   my $exp = shift @_;
236   my $v = cfg_uc @_;
237   my $got = ref($v) || 'scalar';
238   return $v if !defined($v) || $got eq $exp;
239   badcfg @_, "found \L$got\E, expected \L$exp\E";
240   # ^ toml doesn't make refs to scalars, so this is unambiguous
241 }
242
243 sub cfgn {
244   my $exp = shift @_;
245   (cfge $exp, @_) // badcfg @_, "missing";
246 }
247
248 sub cfgs  { cfge 'scalar', @_ }
249 sub cfgsn { cfgn 'scalar', @_ }
250
251 sub cfg_bool {
252   my $v = cfg_uc @_;
253   return $v if !defined($v) || Types::Serialiser::is_bool $v;
254   badcfg @_, "expected boolean";
255 }
256
257 sub cfgn_list {
258   my $l = cfge 'ARRAY', @_;
259   foreach my $x (@$l) {
260     !ref $x or badcfg @_, "list contains non-scalar element";
261   }
262   @$l
263 }
264
265 sub readnail () {
266   my $nailfile = "../Cargo.nail";
267   open N, '<', $nailfile or die "$self: open $nailfile: $!\n";
268   local ($/) = undef;
269   my $toml = <N> // die "$self: read $nailfile: $!";
270   my $transformed;
271   if ($toml !~ m{^\s*\[/}m &&
272       $toml !~ m{^[^\n\#]*\=}m &&
273       # old non-toml syntax
274       $toml =~ s{^[ \t]*([-_0-9a-z]+)[ \t]+(\S+)[ \t]*$}{$1 = \"$2\"}mig) {
275     $toml =~ s{^}{[packages\]\n};
276     my @sd;
277     $toml =~ s{^[ \t]*\-[ \t]*\=[ \t]*(\"[-_0-9a-z]+\"\n?)$}{
278       push @sd, $1; '';
279     }mige;
280     $toml = "subdirs = [\n".(join '', map { "$_\n" } @sd)."]\n".$toml;
281     $transformed = 1;
282   }
283   my $e;
284   ($nail,$e) = from_toml($toml);
285   if (!defined $nail) {
286     if ($transformed) {
287       $toml =~ s/^/    /mg;
288       print STDERR "$self: $nailfile transformed into TOML:\n$toml\n";
289     }
290     $/="\n"; chomp $e;
291     die "$self: parse $nailfile: $e\n";
292   }
293   die "$e ?" if length $e;
294
295   $nail->{subdirs} //= [ ];
296
297   if (!ref $nail->{subdirs}) {
298     $nail->{subdirs} = [
299       grep /^[^\#]/,
300       map { s/^\s+//; s/\s+$//; $_; }
301       split m{\n},
302       $nail->{subdirs}
303     ];
304   }
305
306   unshift @configs, $nail;
307 }
308
309 sub get_dependency_tables ($) {
310   my ($toml) = @_;
311   my @keys = qw(dependencies build-dependencies dev-dependencies);
312   my @r;
313   my $process = sub {
314     my ($node) = @_;
315     foreach my $k (@keys) {
316       my $deps = $node->{$k};
317       push @r, $deps if $deps;
318     }
319   };
320   $process->($toml);
321   foreach my $target_node (values %{ $toml->{target} // { } }) {
322     $process->($target_node);
323   }
324   @r;
325 }
326
327 our @alt_cargo_lock_stat;
328
329 sub consider_alt_cargo_lock () {
330   my @ck = qw(alt_cargo_lock);
331   # User should *either* have Cargo.lock in .gitignore,
332   # or expect to commit Cargo.lock.example ($alt_cargo_lock)
333
334   return unless $do_cargo_lock;
335
336   $alt_cargo_lock = (cfg_uc @ck);
337
338   my $force = 0;
339   if (defined($alt_cargo_lock) && ref($alt_cargo_lock) eq 'HASH') {
340     $force = cfg_bool qw(alt_cargo_lock force);
341     my @ck = qw(alt_cargo_lock file);
342     $alt_cargo_lock = cfg_uc @ck;
343   }
344   $alt_cargo_lock //= Types::Serialiser::true;
345
346   if (Types::Serialiser::is_bool $alt_cargo_lock) {
347     if (!$alt_cargo_lock) { $alt_cargo_lock = undef; return; }
348     $alt_cargo_lock = 'Cargo.lock.example';
349   }
350
351   if (ref($alt_cargo_lock) || $alt_cargo_lock =~ m{/}) {
352     badcfg @ck, "expected boolean, or leafname";
353   }
354
355   if (!stat_exists $alt_cargo_lock, "alt_cargo_lock") {
356     $alt_cargo_lock = undef unless $force;
357     return;
358   }
359   
360   @alt_cargo_lock_stat = stat _;
361 }
362
363 our $oot_dir;      # oot.dir or "Build"
364 our $oot_absdir;
365
366 sub consider_oot () {
367   $oot_dir = cfgs qw(oot dir);
368   my $use = cfgs qw(oot use);
369   unless (defined($oot_dir) || defined($use) ||
370           defined(cfg_uc qw(oot user))) {
371     return;
372   }
373   if (($use//'') eq 'disable') {
374     $oot_dir = undef;
375     return;
376   }
377   $oot_clean //= cfg_bool qw(oot clean);
378   $oot_dir //= 'Build';
379   $oot_absdir = ($oot_dir !~ m{^/} ? "$worksphere/" : ""). $oot_dir;
380 }
381
382 our %manifests;
383 our %packagemap;
384 our %workspaces;
385 our @queued_paths;
386
387 sub read_manifest ($$$) {
388   my ($subdir, $org_subdir, $why) = @_;
389   my $manifest = "../$subdir/Cargo.toml";
390   print STDERR "$self: reading $manifest...\n" if $verbose>=4;
391   if (defined $manifests{$manifest}) {
392     print STDERR
393  "$self: warning: $subdir: specified more than once!".
394  " (ignoring $why)\n";
395     return undef;
396   }
397   foreach my $try ("$manifest.unnailed", "$manifest") {
398     my $toml = toml_or_enoent($try, "manifest, in $why") // next;
399     my $ws = $toml->{workspace};
400     if ($ws) {
401       queue_workspace_members($subdir, $org_subdir, $ws, "$subdir, $why");
402     }
403     my $p = $toml->{package}{name};
404     if (!defined $p and !defined $ws) {
405       print STDERR
406  "$self: warning: $subdir, $why: missing package.name in $try, ignoring\n";
407       next;
408     }
409     $manifests{$manifest} = [ $toml, $org_subdir ] if $p;
410     foreach my $dep (get_dependency_tables $toml) {
411       next unless defined $dep->{path};
412       queue_referenced_path($dep->{path}, $org_subdir,
413                             "dependency of $subdir, $why");
414     }
415     return ($p, $ws);
416   }
417   return undef;
418 }
419
420 sub queue_workspace_members ($$) {
421   my ($subdir, $org_subdir, $ws_toml, $what) = @_;
422   # We need to (more or less) reimplement the cargo workspace
423   # membership algorithm (see the "workspaces" section of the cargo
424   # reference).  How tiresome.
425   #
426   # It's not quite the same for us because we aren't interested in
427   # whether cargo thinks things are "in the workspace".  But we do
428   # need to do the automatic discover.
429
430   my @include = @{ $ws_toml->{members} // [ ] };
431   my $exclude = $ws_toml->{exclude} // [ ];
432
433   my @exclude = map {
434     s/[^*?0-9a-zA-Z_]/\\$&/g;
435     s/\?/./g;
436     s/\*/.*/g;
437   } @$exclude;
438
439   foreach my $spec (@include) {
440     if ($spec =~ m{^/}) {
441       print STDERR
442         "$self: warning: absolute workspace member $spec in $what (not nailing, but cargo will probably use it)\n";
443       next;
444     }
445     my $spec_glob = "../$subdir/$spec";
446     my $globflags = GLOB_ERR|GLOB_BRACE|GLOB_NOMAGIC;
447     foreach my $globent (bsd_glob($spec_glob, $globflags)) {
448       next if grep { $globent =~ m{^$_$} } @exclude;
449       queue_referenced_path($globent, $org_subdir,
450                             "member of workspace $what");
451     }
452   }
453 }
454
455 sub queue_referenced_path ($$$) {
456   my ($spec_path, $org_subdir, $why) = @_;
457   open REALPATH, "-|",
458     qw(realpath), "--relative-to=../$org_subdir", "--", $spec_path
459     or die "$self: fork/pipe/exec for realpath(1)\n";
460   my $rel_path = do { local $/=undef; <REALPATH>; };
461   $?=0; $!=0;
462   my $r = close(REALPATH);
463   die "$self: reap realpath: $!\n" if $!;
464   if (!chomp($rel_path) or $?) {
465     print STDERR
466  "$self: warning: failed to determine realpath for $spec_path in $org_subdir (exit code $?)\n";
467     return;
468   }
469   if ($rel_path =~ m{^\.\./} or $rel_path eq '..') {
470     print STDERR
471       "$self: warning: $spec_path ($why) points outside $org_subdir, not following so not nailing (although cargo probably will follow it)\n";
472     return;
473   }
474
475   my $q_subdir = "$org_subdir/$rel_path";
476   print STDERR "$self: making a note to look at $q_subdir, $why)\n"
477     if $verbose >= 4;
478
479   push @queued_paths, [ "$q_subdir", $org_subdir, $why ];
480 }
481
482 sub readorigs () {
483   # We (and our callees) populate %packagemap and %manifest, so if we
484   # don't run, they remain empty and nothing is nailed.
485   return unless $do_nail;
486
487   foreach my $p (keys %{ $nail->{packages} }) {
488     my $v = $nail->{packages}{$p};
489     my $subdir = ref($v) ? $v->{subdir} : $v;
490     my ($gotpackage, $ws) = read_manifest($subdir, $subdir, "from [packages]");
491     $gotpackage //= '<nothing!>';
492     if ($gotpackage ne $p) {
493       print STDERR
494  "$self: warning: honouring Cargo.nail packages.$subdir=$p even though $subdir contains package $gotpackage!\n";
495     }
496     die if defined $packagemap{$p};
497     $packagemap{$p} = [ $subdir, $subdir ];
498   }
499   foreach my $subdir (@{ $nail->{subdirs} }) {
500     my ($gotpackage,$ws) = read_manifest($subdir, $subdir, "from [subdirs]");
501     if (!defined $gotpackage) {
502       print STDERR
503  "$self: warning: ignoring subdir $subdir which has no (suitable) Cargo.toml\n"
504         unless $ws;
505       next;
506     }
507     $packagemap{$gotpackage} //= [ $subdir, $subdir ];
508   }
509   while (my ($subdir, $org_subdir, $why) = @{ shift @queued_paths or [] }) {
510     next if $manifests{"../$subdir/Cargo.toml"};
511     my ($gotpackage, $ws) = read_manifest($subdir, $org_subdir, $why);
512     next unless $gotpackage;
513     $packagemap{$gotpackage} //= [ $subdir, $org_subdir ];
514   }
515 }
516
517 sub calculate () {
518   foreach my $p (sort keys %packagemap) {
519     print STDERR "$self: package $p in $packagemap{$p}[0]\n" if $verbose>=2;
520   }
521   foreach my $mf (keys %manifests) {
522     die "internal error" unless $do_nail; # belt and braces
523
524     my ($toml, $mf_org_subdir) = @{ $manifests{$mf} };
525     foreach my $deps (get_dependency_tables $toml) {
526       next unless $deps;
527       foreach my $p (keys %packagemap) {
528         my $info = $deps->{$p};
529         next unless defined $info;
530         next if $packagemap{$p}[1] eq $mf_org_subdir;
531         $deps->{$p} = $info = { } unless ref $info;
532         my $oldpath = $info->{path};
533         delete $info->{version};
534         my $newpath = $worksphere.'/'.$packagemap{$p}[0];
535         print STDERR "in $mf set $p path=$newpath (was ".
536           ($oldpath // '<unset>').")\n"
537           if $verbose >= 4;
538         $info->{path} = $newpath;
539         delete $info->{git};
540         delete $info->{branch};
541       }
542     }
543     my $nailing = "$mf.nailing~";
544     unlink_or_enoent $nailing or die "$self: remove old $nailing: $!\n";
545     open N, '>', $nailing or die "$self: create new $nailing: $!\n";
546     print N to_toml($toml) or die "$self: write new $nailing: $!\n";
547     close N or die "$self: close new $nailing: $!\n";
548   }
549 }
550
551 sub addargs () {
552   if ($just_linkfarm) {
553     die "$self: --just-linkfarm but not doing out-of-tree builds!\n"
554       unless defined $oot_dir;
555     @ARGV = ();
556     return;
557   }
558
559   if (!defined $online) {
560     $_ = cfg_uc qw(misc online);
561     if (!defined $_) {
562     } elsif (Types::Serialiser::is_bool $_) {
563       $online = $_;
564     } elsif (ref $_) {
565     } elsif (m/^a/) {
566       $online = undef;
567     } elsif (m/^[1ty]/) { # allow booleanish strings
568       $online = 1;        # for less user frustration
569     } elsif (m/^[0fn]/) {
570       $online = 0;
571     } else {
572       badcfg qw(misc online), "expected boolean or 'auto', found '$_'";
573     }
574   }
575   $online //= 1 if subcmd_p('online');
576   $online //= 0;
577
578   if (subcmd_p('linkfarm-pristine')) {
579     $linkfarm_depth //= 'git';
580     $oot_preclean //= 'src';
581   }
582
583   $cargo_lock_update //= subcmd_p('lock-update');
584   $linkfarm_depth //=
585     subcmd_p('linkfarm-shallow') ? 'shallow' :
586     $cargo_lock_update           ? 'shallow' :
587     '';
588
589   $oot_preclean //= 'no';
590
591   our @add;
592
593   if (!$cargo_lock_update) {
594     push @add, qw(--locked) unless subcmd_p('!locked');
595   }
596   if ($linkfarm_depth eq '') {
597     if (defined($oot_dir) && !subcmd_p('!manifest-path')) {
598       my $cargotoml = "${src_absdir}/Cargo.toml";
599       push @args_preface, "--manifest-path=$cargotoml" if $pass_options;
600       push @add, qw(--target-dir=target) unless subcmd_p('!target-dir');
601     }
602   }
603
604   if (defined($target) && !subcmd_p('!target')) {
605     if ($target =~ m{^[A-Z]}) {
606       $target = (cfgs 'arch', $target) // $archmap{$target}
607         // die "$self: --target=$target alias specified; not in cfg or map\n";
608     }
609     push @add, "--target=$target";
610   }
611
612   push @add, "--offline" unless $online || subcmd_p('!offline');
613
614   if (subcmd_p('edits') && $linkfarm_depth ne 'copy-edit') {
615     print STDERR
616  "$self: *WARNING*: this subcommand expects to edit the source code; you probably want to specify --edits-sources aka -E (which is not the default even now, for safety reasons)\n";
617   }
618
619   push @args_preface, @add if $pass_options;
620   die if grep { m/ / } @add;
621   $ENV{NAILINGCARGO_CARGO_OPTIONS} = "@add";
622
623   unshift @ARGV, @args_preface;
624 }
625
626 our $build_absdir; # .../Build/<subdir>
627
628 sub oot_massage_cmdline () {
629   return unless defined $oot_dir;
630
631   my $use = cfgs qw(oot use);
632   $use // die "$self: out-of-tree build, but \`oot.use' not configured\n";
633   $build_absdir = "$oot_absdir/$subdir";
634
635   my ($pre,$post) = ('','');
636   my @xargs;
637   if ($linkfarm_depth eq '') {
638     push @xargs, $build_absdir;
639     ($pre, $post) = ('cd "$1"; shift; ', '');
640   } else {
641     push @xargs, $oot_absdir, $subdir, $src_absdir;
642     $pre = <<'END_BOTH';
643         bld="$1"; shift; sd="$1"; shift; src="$1"; shift;  
644         cd "$bld"; mkdir -p -- "$sd"; cd "$sd";
645 END_BOTH
646     if ($oot_preclean ne 'no') {
647       $pre.= "find . -maxdepth 1 ! -path .";
648       $pre.= " ! -path ./target" if $oot_preclean ne 'full';
649       $pre.= " -print0 | xargs -0r rm -r --;"
650     }
651     if ($linkfarm_depth eq 'shallow') {
652       $pre.= <<'END_SHALLOW';
653         clean () { find -lname "$src/*" -print0 | xargs -0r rm --; }; clean;
654         find "$src" -maxdepth 1 \! -name Cargo.lock -print0 |
655         xargs -0r sh -ec 'for f in "$@"; do
656                 rm -rf "${f##*/}";
657                 ln -sf -- "$f" .;
658         done';
659 END_SHALLOW
660     } elsif ($linkfarm_depth =~ /full|git/) {
661       $pre .= <<'END_EITHER_DEEP_DIRS';
662         clean () { find -follow -lname "$src/*" -print0 | xargs -0r rm --; };
663         (set -e; cd "$src"; find . \! -name Cargo.lock \! \( -name .git -prune \) \! -path . \! -name .git -type d -print0) |
664         xargs -0r sh -ec 'for f in "$@"; do
665                 rm -f "$f" 2>/dev/null ||:;
666                 mkdir -p "$f";
667         done' x;
668 END_EITHER_DEEP_DIRS
669       if ($linkfarm_depth eq 'git') {
670         $pre .= <<'END_FILES_GIT'
671         (set -e; cd "$src"; git ls-files --exclude-standard -co -z) |
672 END_FILES_GIT
673       } elsif ($linkfarm_depth eq 'full') {
674         $pre .= <<'END_FILES_FULL'
675         (set -e; cd "$src"; find . \! -name Cargo.lock \! \( -name .git -prune \) \! -type d -print0) |
676 END_FILES_FULL
677       }
678       $pre .= <<'END_DEEP';
679         perl -0 -ne '
680                 BEGIN { $src=shift @ARGV; }
681                 next if (readlink "$_"//"") eq "$src/$_";
682                 unlink "$_";
683                 symlink "$src/$_", "$_" or die "$_ $!";
684         ' "$src";
685 END_DEEP
686     } elsif ($linkfarm_depth eq 'copy-edit') {
687       $pre .= <<'END_COPY_EDIT';
688         find -lname "$src/*" -print0 | xargs -0r rm --;
689         (set -e; cd "$src"; git ls-files -c -z |
690         cpio --quiet -p0m --no-preserve-owner -u --make-directories "$bld/$sd");
691         clean () {
692           (set -e; cd "$src"; git ls-files -c -z) | xargs -0r rm -f --;
693         };
694 END_COPY_EDIT
695       $post .= <<'END_COPY_EDIT_BUNDLE';
696         (set -e; cd "$src"; git ls-files -c -z) |
697         cpio -Hustar -o0 --quiet >"nailing-cargo-update.tar";
698 END_COPY_EDIT_BUNDLE
699     } else {
700        die "$linkfarm_depth ?";
701     }
702     $pre .= <<'ENDLK' if $do_cargo_lock;
703         if test -e Cargo.lock; then
704           rm -f Cargo.lock;
705           cp -- "$src"/Cargo.lock .;
706         fi;
707 ENDLK
708     $post .= <<'ENDCLEAN' if $oot_clean && !$just_linkfarm;
709         clean;
710 ENDCLEAN
711   }
712   my $addpath = (cfg_uc qw(oot path_add)) //
713     $use eq 'really' ? Types::Serialiser::true : Types::Serialiser::false;
714   $addpath =
715     !Types::Serialiser::is_bool $addpath ? $addpath           :
716     $addpath                             ? '$HOME/.cargo/bin' :
717                                            undef;
718   if (defined $addpath) {
719     $pre .= <<END
720         PATH=$addpath:\${PATH-/usr/local/bin:/bin:/usr/bin};
721         export PATH;
722 END
723   }
724   $pre  =~ s/^\s+//mg; $pre  =~ s/\s+/ /g;
725   $post =~ s/^\s+//mg; $post =~ s/\s+/ /g;
726
727   my $getuser = sub { cfgsn qw(oot user) };
728   my @command;
729   my $xe = $verbose >= 2 ? 'xe' : 'e';
730   my $sh_ec = sub {
731     if (!length $post) {
732       @command = (@_, 'sh',"-${xe}c",$pre.'exec "$@"','--',@xargs);
733     } else {
734       @command = (@_, 'sh',"-${xe}c",$pre.'"$@"; '.$post,'--',@xargs);
735     }
736     push @command, @ARGV;
737   };
738   my $command_sh = sub {
739     my $quoted = join ' ', map {
740       return $_ if !m/\W/;
741       s/\'/\'\\'\'/g;
742       "'$_'"
743     } @ARGV;
744     @command = @_, "set -${xe}; $pre $quoted; $post";
745   };
746   print STDERR "$self: out-of-tree, building in: \`$build_absdir'\n"
747     if $verbose;
748   if ($use eq 'really') {
749     my $user = $getuser->();
750     my @pw = getpwnam $user or die "$self: oot.user \`$user' lookup failed\n";
751     my $homedir = $pw[7];
752     $sh_ec->('really','-u',$user,'env',"HOME=$homedir");
753     print STDERR "$self: using really to run as user \`$user'\n" if $verbose;
754   } elsif ($use eq 'ssh') {
755     my $user = $getuser->();
756     $user .= '@localhost' unless $user =~ m/\@/;
757     $command_sh->('ssh',$user);
758     print STDERR "$self: using ssh to run as \`$user'\n" if $verbose;
759   } elsif ($use eq 'command_args') {
760     my @c = cfgn_list qw(oot command);
761     $sh_ec->(@c);
762     print STDERR "$self: out-of-tree, adverbial command: @c\n" if $verbose;
763   } elsif ($use eq 'command_sh') {
764     my @c = cfgn_list qw(oot command);
765     $command_sh->(@c);
766     print STDERR "$self: out-of-tree, ssh'ish command: @c\n" if $verbose;
767   } elsif ($use eq 'null') {
768     $sh_ec->();
769   } else {
770     die "$self: oot.use mode $use not recognised\n";
771   }
772   die unless @command;
773   @ARGV = @command;
774 }
775
776 sub setenvs () {
777   $ENV{CARGO_MANIFEST_DIR} = $src_absdir unless $linkfarm_depth;
778   $ENV{NAILINGCARGO_MANIFEST_DIR} = $src_absdir;
779   $ENV{NAILINGCARGO_WORKSPHERE}   = $worksphere;
780   $ENV{NAILINGCARGO_BUILDSPHERE}  = $oot_absdir;
781   delete $ENV{NAILINGCARGO_BUILDSPHERE} unless $oot_absdir;
782   $ENV{NAILINGCARGO_BUILD_DIR}    = $build_absdir // $src_absdir;
783 }
784
785 our $want_uninstall;
786
787 END {
788   if ($want_uninstall) {
789     local ($?);
790     foreach my $mf (keys %manifests) {
791       eval { uninstall1($mf,1); 1; } or warn "$@";
792     }
793     eval { unaltcargolock(1); 1; } or warn "$@";
794   }
795 }
796
797 sub consider_directories () {
798   return unless defined $oot_dir;
799   my $bsubdir = "../$oot_dir/$subdir";
800   return if stat $bsubdir;
801   die "$0: build directory $bsubdir inaccessible\n"
802     unless $!==ENOENT;
803   return if $cargo_lock_update; # will make it
804   die "$0: build directory $bsubdir does not exist, and not in Cargo.lock update mode!\n";
805 }
806
807 our $cleanup_cargo_lock;
808 sub makebackups () {
809   foreach my $mf (keys %manifests) {
810     link "$mf", "$mf.unnailed" or $!==EEXIST
811       or die "$self: make backup link $mf.unnailed: $!\n";
812   }
813
814   if (defined($alt_cargo_lock)) {
815     die 'internal error' unless $do_cargo_lock;
816     if (@alt_cargo_lock_stat) {
817       print STDERR "$self: using alt_cargo_lock `$alt_cargo_lock'..."
818         if $verbose>=3;
819       if (link $alt_cargo_lock, 'Cargo.lock') {
820         print STDERR " linked\n" if $verbose>=3;
821       } elsif ($! != EEXIST) {
822         print STDERR "\n" if $verbose>=3;
823         die "$self: make \`Cargo.lock' available as \`$alt_cargo_lock': $!\n";
824       } else {
825         print STDERR "checking quality." if $verbose>=3;
826         my @lock_stat = stat 'Cargo.lock'
827           or die "$self: stat Cargo.lock (for alt check: $!\n";
828         same_file(\@alt_cargo_lock_stat, \@lock_stat)
829           or die
830 "$self: \`Cargo.lock' and alt file \`$alt_cargo_lock' both exist and are not the same file!\n";
831       }
832       $cleanup_cargo_lock = 1;
833     } else {
834       $cleanup_cargo_lock = 1;
835       # If Cargo.lock exists and alt doesn't, that means either
836       # that a previous run was interrupted, or that the user has
837       # messed up.
838     }
839   }
840 }
841
842 sub nailed ($) {
843   my ($mf) = @_;
844   my $nailed  = "$mf.nailed~"; $nailed =~ s{/([^/]+)$}{/.$1} or die;
845   $nailed;
846 }    
847
848 sub install () {
849   my @our_unfound_stab = stat_exists('Cargo.toml', 'local Cargo.toml')
850     ? (stat _) : ();
851   foreach my $mf (keys %manifests) {
852     if (@our_unfound_stab) {
853       if (stat_exists $mf, "manifest in to-be-nailed directory") {
854         my @mf_stab = stat _ ;
855         if ("@mf_stab[0..1]" eq "@our_unfound_stab[0..1]") {
856           @our_unfound_stab = ();
857         }
858       }
859     }
860
861     my $nailing = "$mf.nailing~";
862     my $nailed = nailed($mf);
863     my ($use, $rm);
864     my $diff;
865     if (open NN, '<', $nailed) {
866       $diff = compare($nailing, \*NN);
867       die "$self: compare $nailing and $nailed: $!" if $diff<0;
868     } else {
869       $!==ENOENT or die "$self: check previous $nailed: $!\n";
870       $diff = 1;
871     }
872     if ($diff) {
873       $use = $nailing;
874       $rm  = $nailed;
875     } else {
876       $use = $nailed;
877       $rm  = $nailing;
878     }
879     rename $use, $mf or die "$self: install nailed $use: $!\n";
880     unlink_or_enoent $rm or die "$self: remove old $rm: $!\n";
881     print STDERR "$self: nailed $mf\n" if $verbose>=3;
882   }
883
884   if (@our_unfound_stab && $do_nail) {
885     print STDERR
886  "$self: *WARNING* cwd is not in Cargo.nail thbough it has Cargo.toml!\n";
887   }
888 }
889
890 sub invoke () {
891   my $r = system @ARGV;
892   if (!$r) {
893     return 0;
894   } elsif ($r<0) {
895     print STDERR "$self: could not execute $ARGV[0]: $!\n";
896     return 127;
897   } elsif ($r & 0xff00) {
898     print STDERR "$self: $ARGV[0] failed (exit status $r)\n";
899     return $r >> 8;
900   } else {
901     print STDERR "$self: $ARGV[0] died due to signal! (wait status $r)\n";
902     return 125;
903   }
904 }
905
906 sub files_return_after_update () {
907   if ($linkfarm_depth eq 'copy-edit') {
908     system qw(sh -ec), <<'END', 'x', "$build_absdir";
909       git ls-files -c -z | \
910       tar -x --keep-newer-files --no-same-permissions --no-same-owner \
911         --no-acls --no-selinux --no-xattrs --warning=no-ignore-newer \
912         -Hustar --null --files-from=- --force-local \
913         -f "$1/nailing-cargo-update.tar"
914 END
915   } elsif ($do_cargo_lock && $cargo_lock_update && !$just_linkfarm) {
916     # avoids importing File::Copy and the error handling is about as good
917     $!=0; $?=0;
918     my $r= system qw(cp --), "$build_absdir/Cargo.lock", "Cargo.lock";
919     die "$self: run cp: $! $?" if $r<0 || $r & 0xff;
920     die "$self: failed to update local Cargo.lock (wait status $r)\n" if $r;
921   }
922 }
923
924 sub uninstall1 ($$) {
925   my ($mf, $enoentok) = @_;
926   my $unnailed = "$mf.unnailed";
927   rename $unnailed, $mf or ($enoentok && $!==ENOENT)
928     or die "$self: failed to restore: rename $unnailed back to $mf: $!\n";
929 }
930
931 sub unaltcargolock ($) {
932   my ($enoentok) = @_;
933   return unless $cleanup_cargo_lock;
934   die 'internal error!' unless $do_cargo_lock && defined $alt_cargo_lock;
935
936   # we ignore $enoentok because we don't know if one was supposed to
937   # have been created.
938
939   rename('Cargo.lock', $alt_cargo_lock) or $!==ENOENT or die
940  "$self: cleanup: rename possibly-updated \`Cargo.lock' to \`$alt_cargo_lock': $!\n";
941
942   unlink 'Cargo.lock' or $!==ENOENT or die
943  "$self: cleanup: remove \`Cargo.lock' in favour of \`$alt_cargo_lock': $!\n";
944   # ^ this also helps clean up the stupid rename() corner case
945 }
946
947 sub uninstall () {
948   foreach my $mf (keys %manifests) {
949     my $nailed = nailed($mf);
950     link $mf, $nailed or die "$self: preserve (link) $mf as $nailed: $!\n";
951     uninstall1($mf,0);
952   }
953   unaltcargolock(0);
954 }
955
956 sub parse_args () {
957   my $is_cargo;
958
959   # Loop exit condition:
960   #   $is_cargo is set
961   #   @ARGV contains
962   #    $is_cargo==1   <cargo-command> <cargo-opts> [--] <subcmd>...
963   #    $is_cargo==0   <build-command>...
964
965  OPTS: for (;;) {
966     if (!@ARGV) {
967       die "$self: need cargo subcommand\n"
968         unless $noact || $just_linkfarm;;
969       push @ARGV, "CARGO-SUBCOMMAND"; # dummy, user may see it
970     }
971
972     $_ = shift @ARGV;
973     my $orgopt = $_;
974
975     my $not_a_nailing_opt = sub { # usage 1
976       unshift @ARGV, $orgopt;
977       unshift @ARGV, 'cargo';
978       $is_cargo = 1;
979       no warnings qw(exiting);
980       last OPTS;
981     };
982     $not_a_nailing_opt->() unless m{^-};
983     $not_a_nailing_opt->() if $_ eq '--';
984
985     if ($_ eq '---') { # usage 2 or 3
986       if (!@ARGV) {
987         die "$self: --- must be followed by build command\n" unless $noact;
988         push @ARGV, 'BUILD-COMMAND';
989       }
990       if ($ARGV[0] eq '--') { # usage 3
991         shift;
992         $is_cargo = 0;
993       } elsif (grep { $_ eq '--' } @ARGV) { # usage 2
994         $is_cargo = 1;
995       } elsif ($ARGV[0] =~ m{[^/]*cargo[^/]*$}) { # usage 2
996         $is_cargo = 1;
997       } else {  # usage 3
998         $is_cargo = 0;
999       }
1000       last;
1001     }
1002     if (m{^-[^-]}) {
1003       while (m{^-.}) {
1004         if (s{^-h}{-}) {
1005           print_usage();
1006         } elsif (s{^-v}{-}) {
1007           $verbose++;
1008         } elsif (s{^-q}{-}) {
1009           $verbose=0;
1010         } elsif (s{^-n}{-}) {
1011           $noact++;
1012         } elsif (s{^-s(.+)}{-}s) {
1013           $cargo_subcmd = $1;
1014         } elsif (s{^-([uU])}{-}) {
1015           $cargo_lock_update = $1=~m/[a-z]/;
1016         } elsif (s{^-([cC])}{-}) {
1017           $pass_options = $1=~m/[a-z]/;
1018         } elsif (s{^-D}{-}) {
1019           $dump++;
1020         } elsif (s{^-E}{-}) {
1021           $linkfarm_depth = 'copy-edit';
1022         } elsif (s{^-T(.+)}{-}s) {
1023           $target = $1;
1024         } elsif (s{^-([oO])}{-}) {
1025           $online = $1=~m/[a-z]/;
1026         } else {
1027           die "$self: unknown short option(s) $_\n" unless $_ eq $orgopt;
1028           $not_a_nailing_opt->();
1029         }
1030       }
1031     } elsif (s{^--help$}{}) {
1032       print_usage();
1033     } elsif (s{^--(?:doc|man|manual)?$}{}) {
1034       show_manual();
1035     } elsif (s{^--target=}{}) {
1036       $target = $_;
1037     } elsif (m{^--(on|off)line$}) {
1038       $online = $1 eq 'on';
1039     } elsif (m{^--just-linkfarm(?:=(shallow|git|full))?$}) {
1040       $just_linkfarm = 1;
1041       $linkfarm_depth = $1 if $1;
1042       $cargo_lock_update= 1; # will set $linkfarm_detph to 1 by default
1043     } elsif (m{^--linkfarm(?:=(no|shallow|git|full))?$}) {
1044       $linkfarm_depth = $1 || 'git';
1045     } elsif (m{^--edits?-sources?$}) {
1046       $linkfarm_depth = 'copy-edit';
1047     } elsif (m{^--just-run$}) {
1048       $do_nail = $do_cargo_lock = $do_lock = 0;
1049     } elsif (m{^--(clean|keep)-linkfarm$}) {
1050       $oot_clean = $1 eq 'clean';
1051     } elsif (m{^--(no-)?preclean-linkfarm$}) {
1052       $oot_preclean = $1 ? 'no' : 'src';
1053     } elsif (m{^--preclean-linkfarm=(no|src|full)$}) {
1054       $oot_preclean = $1;
1055     } elsif (m{^--(no-)?nail$}) {
1056       $do_nail = !$1;
1057     } elsif (m{^--(no-)?cargo-lock-manip$}) {
1058       $do_cargo_lock = !$1;
1059     } elsif (m{^--(no-)?concurrency-lock$}) {
1060       $do_lock = !$1;
1061     } elsif (m{^--leave-nailed$}) {
1062       $leave_nailed = 1;
1063     } elsif (s{^--subcommand-props=}{}) {
1064       my @props = split /\,/, $_;
1065       our %subcmd_prop_ok;
1066       if (!%subcmd_prop_ok) {
1067         foreach my $v (\@subcmd_xprops, values %subcmd_props) {
1068           $subcmd_prop_ok{$_}=1 foreach @$v;
1069         };
1070       }
1071       $subcmd_prop_ok{$_}
1072         or die "$self: unknown subcommand property \`$_'\n"
1073         foreach @props;
1074       $cargo_subcmd = \@props;
1075     } elsif (m{^--(no-)?cargo-lock-update}) {
1076       $cargo_lock_update= !!$1;
1077     } else {
1078       $not_a_nailing_opt->();
1079     }
1080   }
1081
1082   $is_cargo // die;
1083   @ARGV || die;
1084
1085   if ($is_cargo) {
1086     @args_preface = shift @ARGV;
1087     while (defined($_ = shift @ARGV)) {
1088       if (!m{^-|^\+}) { unshift @ARGV, $_; last; }
1089       if ($_ eq '--') { last; }
1090       push @args_preface, $_;
1091     }
1092     @ARGV || die "$self: need cargo subcommand\n";
1093     $cargo_subcmd //= $ARGV[0];
1094     $pass_options //= 1;
1095   } else {
1096     $cargo_subcmd //= '';
1097     $pass_options //= 0;
1098   }
1099   push @args_preface, shift @ARGV;
1100
1101   if (!ref($cargo_subcmd)) {
1102     print STDERR " cargo_subcmd lookup $cargo_subcmd\n" if $dump;
1103     $cargo_subcmd = $subcmd_props{$cargo_subcmd} // [ ];
1104   }
1105
1106   print STDERR " cargo_subcmd props @$cargo_subcmd\n" if $dump;
1107   my %cargo_subcmd;
1108   $cargo_subcmd{$_} = 1 foreach @$cargo_subcmd;
1109   $cargo_subcmd = \%cargo_subcmd;
1110 }
1111
1112 parse_args();
1113 loadconfigs();
1114 readnail();
1115 takelock();
1116 consider_alt_cargo_lock();
1117 consider_oot();
1118 readorigs();
1119 calculate();
1120 addargs();
1121 consider_directories();
1122 our @display_cmd = @ARGV;
1123 oot_massage_cmdline();
1124 setenvs();
1125
1126 if ($dump) {
1127   eval '
1128     use Data::Dumper;
1129     print STDERR Dumper(\%manifests) if $dump>=2;
1130     print STDERR Dumper(\%packagemap, \@ARGV,
1131                         { src_absdir => $src_absdir,
1132                           worksphere => $worksphere,
1133                           subdir => $subdir,
1134                           oot_dir => $oot_dir,
1135                           oot_absdir => $oot_absdir,
1136                           build_absdir => $build_absdir });
1137   ' or die $@;
1138 }
1139
1140 exit 0 if $noact;
1141
1142 $want_uninstall = !$leave_nailed;
1143 makebackups();
1144 install();
1145
1146 printf STDERR "$self: nailed (%s manifests, %s packages)%s\n",
1147   (scalar keys %manifests), (scalar keys %packagemap),
1148   (defined($alt_cargo_lock) and ", using `$alt_cargo_lock'")
1149   if $verbose && $do_nail;
1150
1151 print STDERR "$self: invoking: @display_cmd\n" if $verbose;
1152 my $estatus = invoke();
1153
1154 files_return_after_update();
1155
1156 uninstall() unless $leave_nailed;
1157 $want_uninstall = 0;
1158
1159 print STDERR "$self: ".($do_nail ? "unnailed" : "finished")
1160              .".  status $estatus.\n" if $verbose;
1161
1162 exit $estatus;