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