chiark / gitweb /
New scheme for subcommand handling - wip
[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
5 our $self;
6
7 use strict;
8 use POSIX;
9 use Types::Serialiser;
10
11 our %archmap = (
12     RPI => 'arm-unknown-linux-gnueabihf',
13 );
14
15 BEGIN {
16   $self = $0;  $self =~ s{^.*/(?=.)}{};
17   my $deref = $0;
18   while ($deref =~ m{^/}) {
19     my $link = readlink $deref;
20     if (!defined $link) {
21       $! == EINVAL
22         or die "$self: checking our script location $deref: $!\n";
23       $deref =~ s{/[^/]+$}{}
24         or die "$self: unexpected script path: $deref\n";
25       unshift @INC, $deref."/TOML-Tiny/lib";
26       last;
27     }
28     last if $link !~ m{^/};
29     $deref = $link;
30   }
31 }
32
33 use Fcntl qw(LOCK_EX);
34 use File::Compare;
35 use TOML::Tiny::Faithful;
36
37 our $src_absdir = getcwd() // die "$self: getcwd failed: $!\n";
38
39 our $worksphere = $src_absdir;
40 $worksphere =~ s{/([^/]+)$}{}
41   or die "$self: cwd \`$worksphere' unsupported!\n";e
42 our $subdir = $1; # leafname
43
44 our $lockfile = "../.nailing-cargo.lock";
45
46 our $cargo_subcmd;
47 our $alt_cargo_lock;
48 our $online;
49
50 #
51 our %subcmd_props = {
52 # build (default)  =>qw(                                        ),
53  update            =>qw( !target lock-update                    ),
54 'generate-lockfile'=>qw( !target lock-update                    ),
55  fetch             =>qw( !target             online !target-dir ),
56  ''                =>qw( !target !manifest-path !locked !offline );
57 };
58
59 our @configs;
60 our $verbose=1;
61 our ($noact,$dump);
62 our $target;
63
64 sub read_or_enoent ($) {
65   my ($fn) = @_;
66   if (!open R, '<', $fn) {
67     return undef if $!==ENOENT;
68     die "$self: open $fn: $!\n";
69   }
70   local ($/) = undef;
71   my ($r) = <R> // die "$self: read $fn: $!\n";
72   $r;
73 }
74
75 sub stat_exists ($$) {
76   my ($fn, $what) = @_;
77   if (stat $fn) { return 1; }
78   $!==ENOENT or die "$self: stat $what: $fn: $!\n";
79   return 0;
80 }
81
82 sub toml_or_enoent ($$) {
83   my ($f,$what) = @_;
84   my $toml = read_or_enoent($f) // return;
85   print STDERR "Read TOML from $f\n" if $dump;
86   my ($v,$e) = from_toml($toml);
87   if (!defined $v) {
88     chomp $e;
89     die "$self: parse TOML: $what: $f: $e\n";
90   }
91   die "$e ?" if length $e;
92   $v;
93 }
94
95 sub load1config ($) {
96   my ($f) = @_;
97   my $toml = toml_or_enoent($f, "config file");
98   push @configs, $toml if defined $toml;
99 }
100
101 sub loadconfigs () {
102   my $dotfile = ".nailing-cargo.toml";
103   load1config("../Nailing-Cargo.toml");
104   load1config($dotfile);
105   load1config("$ENV{HOME}/$dotfile") if defined $ENV{HOME};
106   load1config("/etc/nailing-cargo/cfg.toml");
107 }
108
109 sub unlink_or_enoent ($) { unlink $_[0] or $!==ENOENT; }
110
111 sub same_file ($$) {
112   my ($x,$y) = @_;
113   "@$x[0..5]" eq "@$y[0..5]";
114 }
115
116 sub takelock () {
117   for (;;) {
118     open LOCK, ">", $lockfile or die "$self: open/create $lockfile: $!\n";
119     flock LOCK, LOCK_EX or die "$self: lock $lockfile: $!\n";
120     my @fstat = stat LOCK or die "$self: fstat: $!\n";
121     my @stat  = stat $lockfile;
122     if (!@stat) {
123       next if $! == ENOENT;
124       die "$self: stat $lockfile: $!\n";
125     }
126     last if same_file(\@fstat,\@stat);
127   }
128 }
129 sub unlock () {
130   unlink $lockfile or die "$self: removing lockfile: $!\n";
131 }
132
133 our $nail;
134
135 sub badcfg {
136   my $m = pop @_;
137   $" = '.';
138   die "$self: config key \`@_': $m\n";
139 }
140
141 sub cfg_uc {
142   foreach my $cfg (@configs) {
143     my $v = $cfg;
144     foreach my $k (@_) {
145       last unless defined $v;
146       ref($v) eq 'HASH' or badcfg @_, "parent key \`$k' is not a hash";
147       $v = $v->{$k};
148     }
149     return $v if defined $v;
150   }
151   return undef;
152 }
153
154 sub cfge {
155   my $exp = shift @_;
156   my $v = cfg_uc @_;
157   my $got = ref($v) || 'scalar';
158   return $v if !defined($v) || $got eq $exp;
159   badcfg @_, "found \L$got\E, expected \L$exp\E";
160   # ^ toml doesn't make refs to scalars, so this is unambiguous
161 }
162
163 sub cfgn {
164   my $exp = shift @_;
165   (cfge $exp, @_) // badcfg @_, "missing";
166 }
167
168 sub cfgs  { cfge 'scalar', @_ }
169 sub cfgsn { cfgn 'scalar', @_ }
170
171 sub cfg_bool {
172   my $v = cfg_uc @_;
173   return $v if !defined($v) || Types::Serialiser::is_bool $v;
174   badcfg @_, "expected boolean";
175 }
176
177 sub cfgn_list {
178   my $l = cfge 'ARRAY', @_;
179   foreach my $x (@$l) {
180     !ref $x or badcfg @_, "list contains non-scalar element";
181   }
182   @$l
183 }
184
185 sub readnail () {
186   my $nailfile = "../Cargo.nail";
187   open N, '<', $nailfile or die "$self: open $nailfile: $!\n";
188   local ($/) = undef;
189   my $toml = <N> // die "$self: read $nailfile: $!";
190   my $transformed;
191   if ($toml !~ m{^\s*\[/}m &&
192       $toml !~ m{^[^\n\#]*\=}m &&
193       # old non-toml syntax
194       $toml =~ s{^[ \t]*([-_0-9a-z]+)[ \t]+(\S+)[ \t]*$}{$1 = \"$2\"}mig) {
195     $toml =~ s{^}{[packages\]\n};
196     my @sd;
197     $toml =~ s{^[ \t]*\-[ \t]*\=[ \t]*(\"[-_0-9a-z]+\"\n?)$}{
198       push @sd, $1; '';
199     }mige;
200     $toml = "subdirs = [\n".(join '', map { "$_\n" } @sd)."]\n".$toml;
201     $transformed = 1;
202   }
203   my $e;
204   ($nail,$e) = from_toml($toml);
205   if (!defined $nail) {
206     if ($transformed) {
207       $toml =~ s/^/    /mg;
208       print STDERR "$self: $nailfile transformed into TOML:\n$toml\n";
209     }
210     $/="\n"; chomp $e;
211     die "$self: parse $nailfile: $e\n";
212   }
213   die "$e ?" if length $e;
214
215   $nail->{subdirs} //= [ ];
216
217   if (!ref $nail->{subdirs}) {
218     $nail->{subdirs} = [
219       grep /^[^\#]/,
220       map { s/^\s+//; s/\s+$//; $_; }
221       split m{\n},
222       $nail->{subdirs}
223     ];
224   }
225
226   unshift @configs, $nail;
227 }
228
229 our @alt_cargo_lock_stat;
230
231 sub consider_alt_cargo_lock () {
232   my @ck = qw(alt_cargo_lock);
233   # User should *either* have Cargo.lock in .gitignore,
234   # or expect to commit Cargo.lock.example ($alt_cargo_lock)
235
236   $alt_cargo_lock = (cfg_uc @ck);
237
238   my $force = 0;
239   if (defined($alt_cargo_lock) && ref($alt_cargo_lock) eq 'HASH') {
240     $force = cfg_bool qw(alt_cargo_lock force);
241     my @ck = qw(alt_cargo_lock file);
242     $alt_cargo_lock = cfg_uc @ck;
243   }
244   $alt_cargo_lock //= Types::Serialiser::true;
245
246   if (Types::Serialiser::is_bool $alt_cargo_lock) {
247     if (!$alt_cargo_lock) { $alt_cargo_lock = undef; return; }
248     $alt_cargo_lock = 'Cargo.lock.example';
249   }
250
251   if (ref($alt_cargo_lock) || $alt_cargo_lock =~ m{/}) {
252     badcfg @ck, "expected boolean, or leafname";
253   }
254
255   if (!stat_exists $alt_cargo_lock, "alt_cargo_lock") {
256     $alt_cargo_lock = undef unless $force;
257     return;
258   }
259   
260   @alt_cargo_lock_stat = stat _;
261 }
262
263 our $oot_dir;      # oot.dir or "Build"
264
265 sub consider_oot () {
266   $oot_dir = cfgs qw(oot dir);
267   my $use = cfgs qw(oot use);
268   unless (defined($oot_dir) || defined($use)) {
269     die "$self: specified --cargo-lock-update but not out-of-tree build!\n"
270       if $cargo_lock_update;
271     $cargo_lock_update=0;
272     return;
273   }
274   if ($use eq 'disable') {
275     $oot_dir = undef;
276     return;
277   }
278   $oot_dir //= 'Build';
279 }
280
281 our %manifests;
282 our %packagemap;
283
284 sub read_manifest ($) {
285   my ($subdir) = @_;
286   my $manifest = "../$subdir/Cargo.toml";
287   print STDERR "$self: reading $manifest...\n" if $verbose>=4;
288   if (defined $manifests{$manifest}) {
289     print STDERR
290  "$self: warning: $subdir: specified more than once!\n";
291     return undef;
292   }
293   foreach my $try ("$manifest.unnailed", "$manifest") {
294     my $toml = toml_or_enoent($try, "package manifest") // next;
295     my $p = $toml->{package}{name};
296     if (!defined $p) {
297       print STDERR
298  "$self: warning: $subdir: missing package.name in $try, ignoring\n";
299       next;
300     }
301     $manifests{$manifest} = $toml;
302     return $p;
303   }
304   return undef;
305 }
306
307 sub readorigs () {
308   foreach my $p (keys %{ $nail->{packages} }) {
309     my $v = $nail->{packages}{$p};
310     my $subdir = ref($v) ? $v->{subdir} : $v;
311     my $gotpackage = read_manifest($subdir) // '<nothing!>';
312     if ($gotpackage ne $p) {
313       print STDERR
314  "$self: warning: honouring Cargo.nail packages.$subdir=$p even though $subdir contains package $gotpackage!\n";
315     }
316     die if defined $packagemap{$p};
317     $packagemap{$p} = $subdir;
318   }
319   foreach my $subdir (@{ $nail->{subdirs} }) {
320     my $gotpackage = read_manifest($subdir);
321     if (!defined $gotpackage) {
322       print STDERR
323  "$self: warning: ignoring subdir $subdir which has no Cargo.toml\n";
324       next;
325     }
326     $packagemap{$gotpackage} //= $subdir;
327   }
328 }
329
330 sub calculate () {
331   foreach my $p (sort keys %packagemap) {
332     print STDERR "$self: package $p in $packagemap{$p}\n" if $verbose>=2;
333   }
334   foreach my $mf (keys %manifests) {
335     my $toml = $manifests{$mf};
336     foreach my $k (qw(dependencies build-dependencies dev-dependencies)) {
337       my $deps = $toml->{$k};
338       next unless $deps;
339       foreach my $p (keys %packagemap) {
340         my $info = $deps->{$p};
341         next unless defined $info;
342         $deps->{$p} = $info = { } unless ref $info;
343         delete $info->{version};
344         $info->{path} = $worksphere.'/'.$packagemap{$p};
345       }
346     }
347     my $nailing = "$mf.nailing~";
348     unlink_or_enoent $nailing or die "$self: remove old $nailing: $!\n";
349     open N, '>', $nailing or die "$self: create new $nailing: $!\n";
350     print N to_toml($toml) or die "$self: write new $nailing: $!\n";
351     close N or die "$self: close new $nailing: $!\n";
352   }
353 }
354
355 sub addargs () {
356   $online = 1 if subcmd_p('online');
357   $online //= cfg_bool qw(misc online);
358   $online //= 0;
359
360   $cargo_lock_update //= subcmd_p('lock-update');
361
362   if (!$cargo_lock_update) {
363     push @ARGV, qw(--locked) unless subcmd_p('!locked');
364     if (defined($oot_dir) && !subcmd_p('!manifest-path')) {
365       push @ARGV, "--manifest-path=${src_absdir}/Cargo.toml";
366       push @ARGV, qw(--target-dir=target) unless subcmd_p('!target');
367     }
368   }
369
370   if (defined($target) && !subcmd_p('!target')) {
371     if ($target =~ m{^[A-Z]}) {
372       $target = (cfgs 'arch', $target) // $archmap{$target}
373         // die "$self: --target=$target alias specified; not in cfg or map\n";
374     }
375     push @ARGV, "--target=$target";
376   }
377
378   push @ARGV, "--offline" unless $online || subcmd_p('!offline');
379 }
380
381 our $oot_absdir;
382 our $build_absdir; # .../Build/<subdir>
383
384 sub oot_massage_cmdline () {
385   return unless defined $oot_dir;
386
387   my $use = cfgs qw(oot use);
388   $oot_absdir = ($oot_dir !~ m{^/} ? "$worksphere/" : ""). $oot_dir;
389   $build_absdir = "$oot_absdir/$subdir";
390
391   my ($pre,$post);
392   my @xargs;
393   if (!$cargo_lock_update) {
394     push @xargs, $build_absdir;
395     ($pre, $post) = ('cd "$1"; shift; ', '');
396   } else {
397     push @xargs, $oot_absdir, $subdir, $src_absdir;
398     $pre =  <<'END';
399         cd "$1"; shift;
400         mkdir -p -- "$1"; cd "$1"; shift;
401         cp -- "$1"/Cargo.toml
402 END
403     $pre .= <<'ENDLK' if stat_exists 'Cargo.lock', 'working cargo lockfile';
404               "$1"/Cargo.lock
405 ENDLK
406     $pre .= <<'ENDCP';
407                               .;
408 ENDCP
409     $pre .= <<'ENDPRE';
410         shift;
411         mkdir -p src; >src/lib.rs; >build.rs
412 ENDPRE
413     $post = <<'ENDPOST';
414         rm -r src Cargo.toml build.rs;
415 ENDPOST
416   }
417   my $addpath = (cfg_uc qw(oot path_add)) //
418     $use eq 'really' ? Types::Serialiser::true : Types::Serialiser::false;
419   $addpath =
420     !Types::Serialiser::is_bool $addpath ? $addpath           :
421     $addpath                             ? '$HOME/.cargo/bin' :
422                                            undef;
423   if (defined $addpath) {
424     $pre .= <<END
425         PATH=$addpath:\${PATH-/usr/local/bin:/bin:/usr/bin};
426         export PATH;
427 END
428   }
429   $pre  =~ s/^\s+//mg; $pre  =~ s/\s+/ /g;
430   $post =~ s/^\s+//mg; $post =~ s/\s+/ /g;
431
432   my $getuser = sub { cfgsn qw(oot user) };
433   my @command;
434   my $xe = $verbose >= 2 ? 'xe' : 'e';
435   my $sh_ec = sub {
436     if (!length $post) {
437       @command = (@_, 'sh',"-${xe}c",$pre.'exec "$@"','--',@xargs);
438     } else {
439       @command = (@_, 'sh',"-${xe}c",$pre.'"$@"; '.$post,'--',@xargs);
440     }
441     push @command, @ARGV;
442   };
443   my $command_sh = sub {
444     my $quoted = join ' ', map {
445       return $_ if !m/\W/;
446       s/\'/\'\\'\'/g;
447       "'$_'"
448     } @ARGV;
449     @command = @_, "set -${xe}; $pre $quoted; $post";
450   };
451   print STDERR "$self: out-of-tree, building in: \`$build_absdir'\n"
452     if $verbose;
453   if ($use eq 'really') {
454     my $user = $getuser->();
455     my @pw = getpwnam $user or die "$self: oot.user \`$user' lookup failed\n";
456     my $homedir = $pw[7];
457     $sh_ec->('really','-u',$user,'env',"HOME=$homedir");
458     print STDERR "$self: using really to run as user \`$user'\n" if $verbose;
459   } elsif ($use eq 'ssh') {
460     my $user = $getuser->();
461     $user .= '@localhost' unless $user =~ m/\@/;
462     $command_sh->('ssh',$user);
463     print STDERR "$self: using ssh to run as \`$user'\n" if $verbose;
464   } elsif ($use eq 'command_args') {
465     my @c = cfgn_list qw(oot command);
466     $sh_ec->(@c);
467     print STDERR "$self: out-of-tree, adverbial command: @c\n" if $verbose;
468   } elsif ($use eq 'command_sh') {
469     my @c = cfgn_list qw(oot command);
470     $command_sh->(@c);
471     print STDERR "$self: out-of-tree, ssh'ish command: @c\n" if $verbose;
472   } elsif ($use eq 'null') {
473     $sh_ec->();
474   } else {
475     die "$self: oot.use mode $use not recognised\n";
476   }
477   die unless @command;
478   @ARGV = @command;
479 }
480
481 sub setenvs () {
482   $ENV{NAILINGCARGO_WORKSPHERE}   = $worksphere;
483   $ENV{NAILINGCARGO_MANIFEST_DIR} = $src_absdir;
484   $ENV{NAILINGCARGO_BUILDSPHERE}  = $oot_absdir;
485   delete $ENV{NAILINGCARGO_BUILDSPHERE} unless $oot_absdir;
486   $ENV{NAILINGCARGO_BUILD_DIR}    = $build_absdir // $src_absdir;
487 }
488
489 our $want_uninstall;
490
491 END {
492   if ($want_uninstall) {
493     local ($?);
494     foreach my $mf (keys %manifests) {
495       eval { uninstall1($mf,1); 1; } or warn "$@";
496     }
497     eval { unaltcargolock(1); 1; } or warn "$@";
498   }
499 }
500
501 our $cleanup_cargo_lock;
502 sub makebackups () {
503   foreach my $mf (keys %manifests) {
504     link "$mf", "$mf.unnailed" or $!==EEXIST
505       or die "$self: make backup link $mf.unnailed: $!\n";
506   }
507
508   if (defined($alt_cargo_lock)) {
509     if (@alt_cargo_lock_stat) {
510       print STDERR "$self: using alt_cargo_lock `$alt_cargo_lock'..."
511         if $verbose>=3;
512       if (link $alt_cargo_lock, 'Cargo.lock') {
513         print STDERR " linked\n" if $verbose>=3;
514       } elsif ($! != EEXIST) {
515         print STDERR "\n" if $verbose>=3;
516         die "$self: make \`Cargo.lock' available as \`$alt_cargo_lock': $!\n";
517       } else {
518         print STDERR "checking quality." if $verbose>=3;
519         my @lock_stat = stat 'Cargo.lock'
520           or die "$self: stat Cargo.lock (for alt check: $!\n";
521         same_file(\@alt_cargo_lock_stat, \@lock_stat)
522           or die
523 "$self: \`Cargo.lock' and alt file \`$alt_cargo_lock' both exist and are not the same file!\n";
524       }
525       $cleanup_cargo_lock = 1;
526     } else {
527       $cleanup_cargo_lock = 1;
528       # If Cargo.lock exists and alt doesn't, that means either
529       # that a previous run was interrupted, or that the user has
530       # messed up.
531     }
532   }
533 }
534
535 sub nailed ($) {
536   my ($mf) = @_;
537   my $nailed  = "$mf.nailed~"; $nailed =~ s{/([^/]+)$}{/.$1} or die;
538   $nailed;
539 }    
540
541 sub install () {
542   my @our_unfound_stab = stat_exists('Cargo.toml', 'local Cargo.toml')
543     ? (stat _) : ();
544   foreach my $mf (keys %manifests) {
545     if (@our_unfound_stab) {
546       if (stat_exists $mf, "manifest in to-be-nailed directory") {
547         my @mf_stab = stat _ ;
548         if ("@mf_stab[0..1]" eq "@our_unfound_stab[0..1]") {
549           @our_unfound_stab = ();
550         }
551       }
552     }
553
554     my $nailing = "$mf.nailing~";
555     my $nailed = nailed($mf);
556     my ($use, $rm);
557     my $diff;
558     if (open NN, '<', $nailed) {
559       $diff = compare($nailing, \*NN);
560       die "$self: compare $nailing and $nailed: $!" if $diff<0;
561     } else {
562       $!==ENOENT or die "$self: check previous $nailed: $!\n";
563       $diff = 1;
564     }
565     if ($diff) {
566       $use = $nailing;
567       $rm  = $nailed;
568     } else {
569       $use = $nailed;
570       $rm  = $nailing;
571     }
572     rename $use, $mf or die "$self: install nailed $use: $!\n";
573     unlink_or_enoent $rm or die "$self: remove old $rm: $!\n";
574     print STDERR "$self: nailed $mf\n" if $verbose>=3;
575   }
576
577   if (@our_unfound_stab) {
578     print STDERR
579  "$self: *WARNING* cwd is not in Cargo.nail thbough it has Cargo.toml!\n";
580   }
581 }
582
583 sub invoke () {
584   my $r = system @ARGV;
585   if (!$r) {
586     return 0;
587   } elsif ($r<0) {
588     print STDERR "$self: could not execute $ARGV[0]: $!\n";
589     return 127;
590   } elsif ($r & 0xff00) {
591     print STDERR "$self: $ARGV[0] failed (exit status $r)\n";
592     return $r >> 8;
593   } else {
594     print STDERR "$self: $ARGV[0] died due to signal! (wait status $r)\n";
595     return 125;
596   }
597 }
598
599 sub cargo_lock_update_after () {
600   if ($cargo_lock_update) {
601     # avoids importing File::Copy and the error handling is about as good
602     $!=0; $?=0;
603     my $r= system qw(cp --), "$build_absdir/Cargo.lock", "Cargo.lock";
604     die "$self: run cp: $! $?" if $r<0 || $r & 0xff;
605     die "$self: failed to update local Cargo.lock (wait status $r)\n" if $r;
606   }
607 }
608
609 sub uninstall1 ($$) {
610   my ($mf, $enoentok) = @_;
611   my $unnailed = "$mf.unnailed";
612   rename $unnailed, $mf or ($enoentok && $!==ENOENT)
613     or die "$self: failed to restore: rename $unnailed back to $mf: $!\n";
614 }
615
616 sub unaltcargolock ($) {
617   my ($enoentok) = @_;
618   return unless $cleanup_cargo_lock;
619   die 'internal error!' unless defined $alt_cargo_lock;
620
621   # we ignore $enoentok because we don't know if one was supposed to
622   # have been created.
623
624   rename('Cargo.lock', $alt_cargo_lock) or $!==ENOENT or die
625  "$self: cleanup: rename possibly-updated \`Cargo.lock' to \`$alt_cargo_lock': $!\n";
626
627   unlink 'Cargo.lock' or $!==ENOENT or die
628  "$self: cleanup: remove \`Cargo.lock' in favour of \`$alt_cargo_lock': $!\n";
629   # ^ this also helps clean up the stupid rename() corner case
630 }
631
632 sub uninstall () {
633   foreach my $mf (keys %manifests) {
634     my $nailed = nailed($mf);
635     link $mf, $nailed or die "$self: preserve (link) $mf as $nailed: $!\n";
636     uninstall1($mf,0);
637   }
638   unaltcargolock(0);
639 }
640
641 sub parse_args () {
642   my $is_cargo;
643
644   # Loop exit condition:
645   #   $is_cargo is set
646   #   @ARGV contains
647   #    $is_cargo==1   <cargo-command> <cargo-opts> [--] <subcmd>...
648   #    $is_cargo==0   <build-command>...
649
650   for (;;) {
651     @ARGV or die "$self: need cargo subcommand\n";
652
653     $_ = shift @ARGV;
654     my $orgopt = $_;
655
656     my $not_a_nailing_opt = sub { # usage 1
657       unshift @ARGV, $orgopt;
658       unshift @ARGV, 'cargo';
659       $is_cargo = 1;
660       no warnings qw(exiting);
661       last;
662     };
663     $not_a_nailing_opt->() unless m{^-};
664     $not_a_nailing_opt->() if $_ eq '--';
665
666     if ($_ eq '---') { # usage 2 or 3
667       die "$self: --- must be followed by build command\n" unless @ARGV;
668       if ($ARGV[0] eq '--') { # usage 3
669         shift;
670         $is_cargo = 0;
671       } elsif (grep { $_ eq '--' } @ARGV) { # usage 2
672         $is_cargo = 1;
673       } elsif ($ARGV[0] =~ m{[^/]*cargo[^/]*$}) { # usage 2
674         $is_cargo = 1;
675       } else {  # usage 3
676         $is_cargo = 0;
677       }
678       last;
679     }
680     if (m{^-[^-]}) {
681       while (m{^-.}) {
682         if (s{^-v}{-}) {
683           $verbose++;
684         } elsif (s{^-q}{-}) {
685           $verbose=0;
686         } elsif (s{^-n}{-}) {
687           $noact++;
688         } elsif (s{^-c(.+)}{-}s) {
689           $cargo_subcmd = $1;
690         } elsif (s{^-C}{-}) {
691           $cargo_subcmd = '';
692         } elsif (s{^-D}{-}) {
693           $dump++;
694         } elsif (s{^-T(.+)}{-}s) {
695           $target = $1;
696         } elsif (s{^-([oO])}{-}) {
697           $online= $1=~m/[a-z]/;
698         } else {
699           die "$self: unknown short option(s) $_\n" unless $_ eq $orgopt;
700           $not_a_nailing_opt->();
701         }
702       }
703     } elsif (s{^--target=}{}) {
704       $target = $_;
705     } elsif (m{^--(on|off)line$}) {
706       $online = $1 eq 'on';
707     } elsif (s{^--subcommand-props=}{}) {
708       my @props = split /\,/, $_;
709       our %subcmd_prop_ok;
710       if (!%subcmd_prop_ok) {
711         for $v in (%subcmd_props) { $subcmd_prop_ok{$_}=1 foreach @$v; };
712       }
713       $subcmd_prop_ok{$_}
714         or die "$self: unknown subcommand property \`$_'\n"
715         foreach @props;
716       $cargo_subcmd = \@props;
717     } elsif (m{^--(no-)?cargo-lock-update}) {
718       $cargo_lock_update= !!$1;
719     } else {
720       $not_a_nailing_opt->();
721     }
722   }
723
724   $is_cargo // die;
725   @ARGV || die;
726
727   if ($is_cargo) {
728     my @cargo_and_opts = shift @ARGV;
729     while (defined($_ = shift @ARGV)) {
730       if (!m{^-}) { unshift @ARGV, $_; last; }
731       if ($_ eq '--') { last; }
732       push @cargo_and_opts, $_;
733     }
734     @ARGV || die "$self: need cargo subcommand\n";
735     $cargo_subcmd //= $ARGV[0];
736     unshift @ARGV, @cargo_and_opts;
737   } else {
738     $cargo_subcmd //= '';
739   }
740
741   if (!ref($cargo_subcmd)) {
742     $cargo_subcmd = $subcmd_props{$cargo_subcmd} // [ ];
743   }
744 }
745
746 parse_args();
747 loadconfigs();
748 takelock();
749 readnail();
750 consider_alt_cargo_lock();
751 consider_oot();
752 readorigs();
753 calculate();
754 addargs();
755 our @display_cmd = @ARGV;
756 oot_massage_cmdline();
757 setenvs();
758
759 if ($dump) {
760   eval '
761     use Data::Dumper;
762     print STDERR Dumper(\%manifests) if $dump>=2;
763     print STDERR Dumper(\%packagemap, \@ARGV,
764                         { src_absdir => $src_absdir,
765                           worksphere => $worksphere,
766                           subdir => $subdir,
767                           oot_dir => $oot_dir,
768                           oot_absdir => $oot_absdir,
769                           build_absdir => $build_absdir });
770   ' or die $@;
771 }
772
773 exit 0 if $noact;
774
775 $want_uninstall = 1;
776 makebackups();
777 install();
778
779 printf STDERR "$self: nailed (%s manifests, %s packages)%s\n",
780   (scalar keys %manifests), (scalar keys %packagemap),
781   (defined($alt_cargo_lock) and ", using `$alt_cargo_lock'")
782   if $verbose;
783
784 print STDERR "$self: invoking: @display_cmd\n" if $verbose;
785 my $estatus = invoke();
786
787 cargo_lock_update_after();
788
789 uninstall();
790 $want_uninstall = 0;
791
792 print STDERR "$self: unnailed.  status $estatus.\n" if $verbose;
793
794 exit $estatus;