chiark / gitweb /
nailing-cargo: offline by default
[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-cargo make
22 #   ../nailing-cargo/nailing-cargo cargo build
23
24 # Why do we need this ?
25 #
26 #  https://github.com/rust-lang/cargo/issues/6713
27 #  https://stackoverflow.com/questions/33025887/how-to-use-a-local-unpublished-crate
28 #  https://github.com/rust-lang/cargo/issues/1481
29
30 # Options:
31 #    -v   Increase verbosity.  Default is 1.
32 #    -q   Set verbosity ot 0.
33 #    -D   Increase amount of debugging dump.
34 #
35 #    -n   "No action": stop after writing Cargo.toml.nailing~
36 #         everywhere, and do not run any build command.
37 #
38 #    -T<arch> --target=<arch>
39 #         Specify target architecture.  If <arch> starts with a
40 #         capital ascii letter, is an alias for some other arch
41 #         looked up in Cargo.nail and then in the builtin list:
42 #           RPI   arm-unknown-linux-gnueabihf
43 #         Translates to a --target= option to the ultimate command,
44 #         unless that is a cargo subcommand which would reject it.
45 #
46 #    -u | --cargo-lock-update
47 #    -U | --no-cargo-lock-update
48 #         Arranges to do a dance to allow Cargo.lock (or
49 #         alternative) to be updated in the source directory.
50 #
51 #         The Cargo.lock and Cargo.toml are copied to the build
52 #         directory along with a skeleton just big enough to fool
53 #         cargo.  After cargo has run, the resulting Cargo.lock is
54 #         copied back to the source tree.
55 #
56 #         Makes no sense with in-tree builds.
57 #
58 #         Default is no update unless the ultimate command is a
59 #         cargo subcommand which we know needs it.
60 #
61 #    -m | --cargo-manifest-args
62 #    -M | --no-cargo-manifest-args
63 #         Controls whether we add cargo command line options
64 #         relating to finding Cargo.toml, to the command to
65 #         run.
66 #
67 #         Default is true if we are doing an out-of- tree build,
68 #         unless we are updating the Cargo.lock (in which case the
69 #         only relevant files are to be found in the build directory).
70 #
71 #         The arguments are
72 #             --manifest-path=<path/to/Cargo.toml>
73 #             --locked
74 #             --target-dir=target
75 #
76 #    -T | --no-cargo-target-arg
77 #    -t | --cargo-target-arg
78 #         Suppress --target (or un-suppress it).  Only useful with -m.
79 #         Done automatically when nailing-cargo sees that the cargo
80 #         subcommand is one which needs it, eg `fetch'.
81 #
82 #    --online | --offline         
83 #         Whether to allow cargo to make network access.
84 #         (nailing-cargo always passes --offline to cargo, unless
85 #         --online is in force).  The default is offline,
86 #         unless the cargo subcommand is one which implies
87 #         online (currently, `fetch').
88 #
89 # Cargo.nail:
90 #
91 #    # Adds each <subdir> to the list of directories whose
92 #    # Cargo.toml is to be nailed, and also arranges to use
93 #    # the package found there for other directories if
94 #    # no other source of that package is evident in Cargo.nail.
95 #    subdirs = [
96 #      "<subdir>"
97 #      ...
98 #    ]
99 # or:
100 #    subdirs = """
101 #      "<subdir>"
102 #      ...
103 #    """
104 #
105 #    # Adds <subdir> to the list of directories whose Cargo.toml
106 #    # is to be nailed, and overrides any other nailing for <package>
107 #    [packages]
108 #    <package> = <subdir>
109 #    <package> = { <subdir> = ... }
110 #
111 # To control use of alternative Cargo.lock filename:
112 #    [alt_cargolock]
113 #
114 #    file = true               # equivalent to "Cargo.lock.example"
115 #    file = false              # disables this feature
116 #    file = "<some leafname>"
117
118 #    force = false    # default, uses alt file only if it already exists
119 #    force = true     # always uses alt file; creation would make Cargo.lock
120 #
121 # (you can also specify just alt_cargo_lock instead of alt_cargo_lock.file)
122 #
123 # To enable out of tree builds:
124 #    [oot]
125 #    dir = "<build-directory>"    # default is Build, if use is specified
126 # and then
127 #    use = "really"
128 #    user = "<someuser>"
129 #  or
130 #    use = "ssh"
131 #    user = "<user>@host"         # NB must still share a filesystem!
132 #  or
133 #    use = "command_args"
134 #    command = ["<command>", "<which works like>", "nice"]
135 #  or
136 #    use = "command_sh"
137 #    command = ["<command>", "<which work like>", "sh -c"]
138 #  or
139 #    use = "null"
140 #
141 # Other settings:
142 #    [misc]
143 #    online = true   # forces default to be --online
144 #    online = false  # forces default to be --offline
145 #
146 # Limitations:
147 #
148 #   Always dirties everyone's Cargo.toml, but tries to put them
149 #     back (if not, running it again should fix it).  Cannnot be
150 #     fixed without changes to cargo.
151 #
152 #   Out of tree builds require a unified filesystem view: eg,
153 #     different users on the same host, NFS, or something.  This
154 #     could be improved.
155 #
156 #   Alternative Cargo.lock file must currently be a leafname.
157 #     I think this just involves review to check other values work.
158 #
159 #   Alternative Cargo.lock file must be on smae filesystem.
160 #     This is not so easy; we would want the existing algorithm but
161 #     a fallback for this case.
162 #
163 #   Cargo.nail unconditionally looked for in ..
164 #     Ideally should be configurable, and also perhaps be able
165 #     to combine multiple Cargo.nail files ?
166 #
167 # Env vars we pass to the command:
168 #   NAILINGCARGO_WORKSPHERE     absolute path of invocation ..
169 #   NAILINGCARGO_MANIFEST_DIR   absolute path of invocation .
170 #   NAILINGCARGO_BUILDSPHERE    only if out of tree: abs parent of build dir
171 #   NAILINGCARGO_BUILD_DIR      absolute path of build dir (even if = src)
172
173
174 our $self;
175
176 use strict;
177 use POSIX;
178 use Types::Serialiser;
179
180 our %archmap = (
181     RPI => 'arm-unknown-linux-gnueabihf',
182 );
183
184 BEGIN {
185   $self = $0;  $self =~ s{^.*/(?=.)}{};
186   my $deref = $0;
187   while ($deref =~ m{^/}) {
188     my $link = readlink $deref;
189     if (!defined $link) {
190       $! == EINVAL
191         or die "$self: checking our script location $deref: $!\n";
192       $deref =~ s{/[^/]+$}{}
193         or die "$self: unexpected script path: $deref\n";
194       unshift @INC, $deref."/TOML-Tiny/lib";
195       last;
196     }
197     last if $link !~ m{^/};
198     $deref = $link;
199   }
200 }
201
202 use Fcntl qw(LOCK_EX);
203 use File::Compare;
204 use TOML::Tiny::Faithful;
205
206 our $src_absdir = getcwd() // die "$self: getcwd failed: $!\n";
207
208 our $worksphere = $src_absdir;
209 $worksphere =~ s{/([^/]+)$}{}
210   or die "$self: cwd \`$worksphere' unsupported!\n";
211 our $subdir = $1; # leafname
212
213 our $lockfile = "../.nailing-cargo.lock";
214
215 our $cargo_lock_update;
216 our $cargo_manifest_args;
217 our $cargo_target_arg=1;
218 our $alt_cargo_lock;
219 our $online;
220
221 our @configs;
222 our $verbose=1;
223 our ($noact,$dump);
224 our $target;
225
226 sub read_or_enoent ($) {
227   my ($fn) = @_;
228   if (!open R, '<', $fn) {
229     return undef if $!==ENOENT;
230     die "$self: open $fn: $!\n";
231   }
232   local ($/) = undef;
233   my ($r) = <R> // die "$self: read $fn: $!\n";
234   $r;
235 }
236
237 sub stat_exists ($$) {
238   my ($fn, $what) = @_;
239   if (stat $fn) { return 1; }
240   $!==ENOENT or die "$self: stat $what: $fn: $!\n";
241   return 0;
242 }
243
244 sub toml_or_enoent ($$) {
245   my ($f,$what) = @_;
246   my $toml = read_or_enoent($f) // return;
247   my ($v,$e) = from_toml($toml);
248   if (!defined $v) {
249     chomp $e;
250     die "$self: parse TOML: $what: $f: $e\n";
251   }
252   die "$e ?" if length $e;
253   $v;
254 }
255
256 sub load1config ($) {
257   my ($f) = @_;
258   my $toml = toml_or_enoent($f, "config file");
259   push @configs, $toml if defined $toml;
260 }
261
262 sub loadconfigs () {
263   my $cfgleaf = ".nailing-cargo-cfg.toml";
264   load1config("/etc/nailing-cargo/cfg.toml");
265   load1config("$worksphere/$cfgleaf");
266   load1config("$ENV{HOME}/$cfgleaf") if defined $ENV{HOME};
267 }
268
269 sub getcfg ($$) {
270   my ($k, $def) = @_;
271   foreach my $cfg (@configs) {
272     my $v = $cfg->{$k};
273     return $v if defined $v;
274   }
275   return $def;
276 }
277
278 sub unlink_or_enoent ($) { unlink $_[0] or $!==ENOENT; }
279
280 sub same_file ($$) {
281   my ($x,$y) = @_;
282   "@$x[0..5]" eq "@$y[0..5]";
283 }
284
285 sub takelock () {
286   for (;;) {
287     open LOCK, ">", $lockfile or die "$self: open/create $lockfile: $!\n";
288     flock LOCK, LOCK_EX or die "$self: lock $lockfile: $!\n";
289     my @fstat = stat LOCK or die "$self: fstat: $!\n";
290     my @stat  = stat $lockfile;
291     if (!@stat) {
292       next if $! == ENOENT;
293       die "$self: stat $lockfile: $!\n";
294     }
295     last if same_file(\@fstat,\@stat);
296   }
297 }
298 sub unlock () {
299   unlink $lockfile or die "$self: removing lockfile: $!\n";
300 }
301
302 our $nail;
303
304 sub badcfg {
305   my $m = pop @_;
306   $" = '.';
307   die "$self: config key \`@_': $m\n";
308 }
309
310 sub cfg_uc {
311   my $v = $nail;
312   foreach my $k (@_) {
313     last unless defined $v;
314     ref($v) eq 'HASH' or badcfg @_, "parent key \`$k' is not a hash";
315     $v = $v->{$k};
316   }
317   return $v;
318 }
319
320 sub cfge {
321   my $exp = shift @_;
322   my $v = cfg_uc @_;
323   my $got = ref($v) || 'scalar';
324   return $v if !defined($v) || $got eq $exp;
325   badcfg @_, "found \L$got\E, expected \L$exp\E";
326   # ^ toml doesn't make refs to scalars, so this is unambiguous
327 }
328
329 sub cfgn {
330   my $exp = shift @_;
331   (cfge $exp, @_) // badcfg @_, "missing";
332 }
333
334 sub cfgs  { cfge 'scalar', @_ }
335 sub cfgsn { cfgn 'scalar', @_ }
336
337 sub cfg_bool {
338   my $v = cfg_uc @_;
339   return $v if !defined($v) || Types::Serialiser::is_bool $v;
340   badcfg @_, "expected boolean";
341 }
342
343 sub cfgn_list {
344   my $l = cfge 'ARRAY', @_;
345   foreach my $x (@$l) {
346     !ref $x or badcfg @_, "list contains non-scalar element";
347   }
348   @$l
349 }
350
351 sub readnail () {
352   my $nailfile = "../Cargo.nail";
353   open N, '<', $nailfile or die "$self: open $nailfile: $!\n";
354   local ($/) = undef;
355   my $toml = <N> // die "$self: read $nailfile: $!";
356   my $transformed;
357   if ($toml !~ m{^\s*\[/}m &&
358       $toml !~ m{^[^\n\#]*\=}m &&
359       # old non-toml syntax
360       $toml =~ s{^[ \t]*([-_0-9a-z]+)[ \t]+(\S+)[ \t]*$}{$1 = \"$2\"}mig) {
361     $toml =~ s{^}{[packages\]\n};
362     my @sd;
363     $toml =~ s{^[ \t]*\-[ \t]*\=[ \t]*(\"[-_0-9a-z]+\"\n?)$}{
364       push @sd, $1; '';
365     }mige;
366     $toml = "subdirs = [\n".(join '', map { "$_\n" } @sd)."]\n".$toml;
367     $transformed = 1;
368   }
369   my $e;
370   ($nail,$e) = from_toml($toml);
371   if (!defined $nail) {
372     if ($transformed) {
373       $toml =~ s/^/    /mg;
374       print STDERR "$self: $nailfile transformed into TOML:\n$toml\n";
375     }
376     $/="\n"; chomp $e;
377     die "$self: parse $nailfile: $e\n";
378   }
379   die "$e ?" if length $e;
380
381   $nail->{subdirs} //= [ ];
382
383   if (!ref $nail->{subdirs}) {
384     $nail->{subdirs} = [
385       grep /^[^\#]/,
386       map { s/^\s+//; s/\s+$//; $_; }
387       split m{\n},
388       $nail->{subdirs}
389     ];
390   }
391 }
392
393 our @alt_cargo_lock_stat;
394
395 sub consider_alt_cargo_lock () {
396   my @ck = qw(alt_cargo_lock);
397   # User should *either* have Cargo.lock in .gitignore,
398   # or expect to commit Cargo.lock.example ($alt_cargo_lock)
399
400   $alt_cargo_lock = (cfg_uc @ck);
401
402   my $force = 0;
403   if (defined($alt_cargo_lock) && ref($alt_cargo_lock) eq 'HASH') {
404     $force = cfg_bool qw(alt_cargo_lock force);
405     my @ck = qw(alt_cargo_lock file);
406     $alt_cargo_lock = cfg_uc @ck;
407   }
408   $alt_cargo_lock //= Types::Serialiser::true;
409
410   if (Types::Serialiser::is_bool $alt_cargo_lock) {
411     if (!$alt_cargo_lock) { $alt_cargo_lock = undef; return; }
412     $alt_cargo_lock = 'Cargo.lock.example';
413   }
414
415   if (ref($alt_cargo_lock) || $alt_cargo_lock =~ m{/}) {
416     badcfg @ck, "expected boolean, or leafname";
417   }
418
419   if (!stat_exists $alt_cargo_lock, "alt_cargo_lock") {
420     $alt_cargo_lock = undef unless $force;
421     return;
422   }
423   
424   @alt_cargo_lock_stat = stat _;
425 }
426
427 our $oot_dir;      # oot.dir or "Build"
428
429 sub consider_oot () {
430   $oot_dir = cfgs qw(oot dir);
431   my $use = cfgs qw(oot use);
432   unless (defined($oot_dir) || defined($use)) {
433     die "$self: specified --cargo-lock-update but not out-of-tree build!\n"
434       if $cargo_lock_update;
435     $cargo_lock_update=0;
436     return;
437   }
438   $oot_dir //= 'Build';
439 }
440
441 our %manifests;
442 our %packagemap;
443
444 sub read_manifest ($) {
445   my ($subdir) = @_;
446   my $manifest = "../$subdir/Cargo.toml";
447   print STDERR "$self: reading $manifest...\n" if $verbose>=4;
448   if (defined $manifests{$manifest}) {
449     print STDERR
450  "$self: warning: $subdir: specified more than once!\n";
451     return undef;
452   }
453   foreach my $try ("$manifest.unnailed", "$manifest") {
454     my $toml = toml_or_enoent($try, "package manifest") // next;
455     my $p = $toml->{package}{name};
456     if (!defined $p) {
457       print STDERR
458  "$self: warning: $subdir: missing package.name in $try, ignoring\n";
459       next;
460     }
461     $manifests{$manifest} = $toml;
462     return $p;
463   }
464   return undef;
465 }
466
467 sub readorigs () {
468   foreach my $p (keys %{ $nail->{packages} }) {
469     my $v = $nail->{packages}{$p};
470     my $subdir = ref($v) ? $v->{subdir} : $v;
471     my $gotpackage = read_manifest($subdir) // '<nothing!>';
472     if ($gotpackage ne $p) {
473       print STDERR
474  "$self: warning: honouring Cargo.nail packages.$subdir=$p even though $subdir contains package $gotpackage!\n";
475     }
476     die if defined $packagemap{$p};
477     $packagemap{$p} = $subdir;
478   }
479   foreach my $subdir (@{ $nail->{subdirs} }) {
480     my $gotpackage = read_manifest($subdir);
481     if (!defined $gotpackage) {
482       print STDERR
483  "$self: warning: ignoring subdir $subdir which has no Cargo.toml\n";
484       next;
485     }
486     $packagemap{$gotpackage} //= $subdir;
487   }
488 }
489
490 sub calculate () {
491   foreach my $p (sort keys %packagemap) {
492     print STDERR "$self: package $p in $packagemap{$p}\n" if $verbose>=2;
493   }
494   foreach my $mf (keys %manifests) {
495     my $toml = $manifests{$mf};
496     foreach my $k (qw(dependencies build-dependencies dev-dependencies)) {
497       my $deps = $toml->{$k};
498       next unless $deps;
499       foreach my $p (keys %packagemap) {
500         my $info = $deps->{$p};
501         next unless defined $info;
502         $deps->{$p} = $info = { } unless ref $info;
503         delete $info->{version};
504         $info->{path} = $worksphere.'/'.$packagemap{$p};
505       }
506     }
507     my $nailing = "$mf.nailing~";
508     unlink_or_enoent $nailing or die "$self: remove old $nailing: $!\n";
509     open N, '>', $nailing or die "$self: create new $nailing: $!\n";
510     print N to_toml($toml) or die "$self: write new $nailing: $!\n";
511     close N or die "$self: close new $nailing: $!\n";
512   }
513 }
514
515 sub addargs () {
516   $online //= cfg_bool qw(misc online);
517
518   if (@ARGV>=2 &&
519       $ARGV[0] =~ m{\bcargo\b}) {
520     if ($ARGV[1] =~ m/^(?:generate-lockfile|update)$/) {
521       $cargo_lock_update //= 1;
522       $target = undef;
523     }
524     if ($ARGV[1] =~ m/^(?:fetch)$/) {
525       $cargo_target_arg=0;
526       $online //= 1;
527     }
528   }
529   $cargo_lock_update //= 0;
530   $cargo_manifest_args //=
531     (defined $oot_dir) && !$cargo_lock_update;
532
533   $online //= 0;
534
535   if ($cargo_manifest_args) {
536     push @ARGV, "--manifest-path=${src_absdir}/Cargo.toml",
537       qw(--locked);
538     push @ARGV, qw(--target-dir=target) if $cargo_target_arg;
539   }
540
541   if (defined $target) {
542     if ($target =~ m{^[A-Z]}) {
543       $target = (cfgs 'arch', $target) // $archmap{$target}
544         // die "$self: --target=$target alias specified; not in cfg or map\n";
545     }
546     push @ARGV, "--target=$target";
547   }
548
549   push @ARGV, "--offline" unless $online;
550 }
551
552 our $oot_absdir;
553 our $build_absdir; # .../Build/<subdir>
554
555 sub oot_massage_cmdline () {
556   return unless defined $oot_dir;
557
558   my $use = cfgs qw(oot use);
559   $oot_absdir = ($oot_dir !~ m{^/} ? "$worksphere/" : ""). $oot_dir;
560   $build_absdir = "$oot_absdir/$subdir";
561
562   my ($pre,$post);
563   my @xargs;
564   if (!$cargo_lock_update) {
565     push @xargs, $build_absdir;
566     ($pre, $post) = ('cd "$1"; shift; ', '');
567   } else {
568     push @xargs, $oot_absdir, $subdir, $src_absdir;
569     $pre =  <<'END';
570         cd "$1"; shift;
571         mkdir -p -- "$1"; cd "$1"; shift;
572         cp -- "$1"/Cargo.toml
573 END
574     $pre .= <<'ENDLK' if stat_exists 'Cargo.lock', 'working cargo lockfile';
575               "$1"/Cargo.lock
576 ENDLK
577     $pre .= <<'ENDCP';
578                               .;
579 ENDCP
580     $pre .= <<'ENDPRE';
581         shift;
582         mkdir -p src; >src/lib.rs; >build.rs
583 ENDPRE
584     $post = <<'ENDPOST';
585         rm -r src Cargo.toml build.rs;
586 ENDPOST
587   }
588   my $addpath = (cfg_uc qw(oot path_add)) //
589     $use eq 'really' ? Types::Serialiser::true : Types::Serialiser::false;
590   $addpath =
591     !Types::Serialiser::is_bool $addpath ? $addpath           :
592     $addpath                             ? '$HOME/.cargo/bin' :
593                                            undef;
594   if (defined $addpath) {
595     $pre .= <<END
596         PATH=$addpath:\${PATH-/usr/local/bin:/bin:/usr/bin};
597         export PATH;
598 END
599   }
600   $pre  =~ s/^\s+//mg; $pre  =~ s/\s+/ /g;
601   $post =~ s/^\s+//mg; $post =~ s/\s+/ /g;
602
603   my $getuser = sub { cfgsn qw(oot user) };
604   my @command;
605   my $xe = $verbose >= 2 ? 'xe' : 'e';
606   my $sh_ec = sub {
607     if (!length $post) {
608       @command = (@_, 'sh',"-${xe}c",$pre.'exec "$@"','--',@xargs);
609     } else {
610       @command = (@_, 'sh',"-${xe}c",$pre.'"$@"; '.$post,'--',@xargs);
611     }
612     push @command, @ARGV;
613   };
614   my $command_sh = sub {
615     my $quoted = join ' ', map {
616       return $_ if !m/\W/;
617       s/\'/\'\\'\'/g;
618       "'$_'"
619     } @ARGV;
620     @command = @_, "set -${xe}; $pre $quoted; $post";
621   };
622   print STDERR "$self: out-of-tree, building in: \`$build_absdir'\n"
623     if $verbose;
624   if ($use eq 'really') {
625     my $user = $getuser->();
626     my @pw = getpwnam $user or die "$self: oot.user \`$user' lookup failed\n";
627     my $homedir = $pw[7];
628     $sh_ec->('really','-u',$user,'env',"HOME=$homedir");
629     print STDERR "$self: using really to run as user \`$user'\n" if $verbose;
630   } elsif ($use eq 'ssh') {
631     my $user = $getuser->();
632     $user .= '@localhost' unless $user =~ m/\@/;
633     $command_sh->('ssh',$user);
634     print STDERR "$self: using ssh to run as \`$user'\n" if $verbose;
635   } elsif ($use eq 'command_args') {
636     my @c = cfgn_list qw(oot command);
637     $sh_ec->(@c);
638     print STDERR "$self: out-of-tree, adverbial command: @c\n" if $verbose;
639   } elsif ($use eq 'command_sh') {
640     my @c = cfgn_list qw(oot command);
641     $command_sh->(@c);
642     print STDERR "$self: out-of-tree, ssh'ish command: @c\n" if $verbose;
643   } elsif ($use eq 'null') {
644     $sh_ec->();
645   } else {
646     die "$self: oot.use mode $use not recognised\n";
647   }
648   die unless @command;
649   @ARGV = @command;
650 }
651
652 sub setenvs () {
653   $ENV{NAILINGCARGO_WORKSPHERE}   = $worksphere;
654   $ENV{NAILINGCARGO_MANIFEST_DIR} = $src_absdir;
655   $ENV{NAILINGCARGO_BUILDSPHERE}  = $oot_absdir;
656   delete $ENV{NAILINGCARGO_BUILDSPHERE} unless $oot_absdir;
657   $ENV{NAILINGCARGO_BUILD_DIR}    = $build_absdir // $src_absdir;
658 }
659
660 our $want_uninstall;
661
662 END {
663   if ($want_uninstall) {
664     local ($?);
665     foreach my $mf (keys %manifests) {
666       eval { uninstall1($mf,1); 1; } or warn "$@";
667     }
668     eval { unaltcargolock(1); 1; } or warn "$@";
669   }
670 }
671
672 our $cleanup_cargo_lock;
673 sub makebackups () {
674   foreach my $mf (keys %manifests) {
675     link "$mf", "$mf.unnailed" or $!==EEXIST
676       or die "$self: make backup link $mf.unnailed: $!\n";
677   }
678
679   if (defined($alt_cargo_lock)) {
680     if (@alt_cargo_lock_stat) {
681       print STDERR "$self: using alt_cargo_lock `$alt_cargo_lock'..."
682         if $verbose>=3;
683       if (link $alt_cargo_lock, 'Cargo.lock') {
684         print STDERR " linked\n" if $verbose>=3;
685       } elsif ($! != EEXIST) {
686         print STDERR "\n" if $verbose>=3;
687         die "$self: make \`Cargo.lock' available as \`$alt_cargo_lock': $!\n";
688       } else {
689         print STDERR "checking quality." if $verbose>=3;
690         my @lock_stat = stat 'Cargo.lock'
691           or die "$self: stat Cargo.lock (for alt check: $!\n";
692         same_file(\@alt_cargo_lock_stat, \@lock_stat)
693           or die
694 "$self: \`Cargo.lock' and alt file \`$alt_cargo_lock' both exist and are not the same file!\n";
695       }
696       $cleanup_cargo_lock = 1;
697     } else {
698       $cleanup_cargo_lock = 1;
699       # If Cargo.lock exists and alt doesn't, that means either
700       # that a previous run was interrupted, or that the user has
701       # messed up.
702     }
703   }
704 }
705
706 sub nailed ($) {
707   my ($mf) = @_;
708   my $nailed  = "$mf.nailed~"; $nailed =~ s{/([^/]+)$}{/.$1} or die;
709   $nailed;
710 }    
711
712 sub install () {
713   foreach my $mf (keys %manifests) {
714     my $nailing = "$mf.nailing~";
715     my $nailed = nailed($mf);
716     my ($use, $rm);
717     my $diff;
718     if (open NN, '<', $nailed) {
719       $diff = compare($nailing, \*NN);
720       die "$self: compare $nailing and $nailed: $!" if $diff<0;
721     } else {
722       $!==ENOENT or die "$self: check previous $nailed: $!\n";
723       $diff = 1;
724     }
725     if ($diff) {
726       $use = $nailing;
727       $rm  = $nailed;
728     } else {
729       $use = $nailed;
730       $rm  = $nailing;
731     }
732     rename $use, $mf or die "$self: install nailed $use: $!\n";
733     unlink_or_enoent $rm or die "$self: remove old $rm: $!\n";
734     print STDERR "$self: nailed $mf\n" if $verbose>=3;
735   }
736 }
737
738 sub invoke () {
739   my $r = system @ARGV;
740   if (!$r) {
741     return 0;
742   } elsif ($r<0) {
743     print STDERR "$self: could not execute $ARGV[0]: $!\n";
744     return 127;
745   } elsif ($r & 0xff00) {
746     print STDERR "$self: $ARGV[0] failed (exit status $r)\n";
747     return $r >> 8;
748   } else {
749     print STDERR "$self: $ARGV[0] died due to signal! (wait status $r)\n";
750     return 125;
751   }
752 }
753
754 sub cargo_lock_update_after () {
755   if ($cargo_lock_update) {
756     # avoids importing File::Copy and the error handling is about as good
757     $!=0; $?=0;
758     my $r= system qw(cp --), "$build_absdir/Cargo.lock", "Cargo.lock";
759     die "$self: run cp: $! $?" if $r<0 || $r & 0xff;
760     die "$self: failed to update local Cargo.lock (wait status $r)\n" if $r;
761   }
762 }
763
764 sub uninstall1 ($$) {
765   my ($mf, $enoentok) = @_;
766   my $unnailed = "$mf.unnailed";
767   rename $unnailed, $mf or ($enoentok && $!==ENOENT)
768     or die "$self: failed to restore: rename $unnailed back to $mf: $!\n";
769 }
770
771 sub unaltcargolock ($) {
772   my ($enoentok) = @_;
773   return unless $cleanup_cargo_lock;
774   die 'internal error!' unless defined $alt_cargo_lock;
775
776   # we ignore $enoentok because we don't know if one was supposed to
777   # have been created.
778
779   rename('Cargo.lock', $alt_cargo_lock) or $!==ENOENT or die
780  "$self: cleanup: rename possibly-updated \`Cargo.lock' to \`$alt_cargo_lock': $!\n";
781
782   unlink 'Cargo.lock' or $!==ENOENT or die
783  "$self: cleanup: remove \`Cargo.lock' in favour of \`$alt_cargo_lock': $!\n";
784   # ^ this also helps clean up the stupid rename() corner case
785 }
786
787 sub uninstall () {
788   foreach my $mf (keys %manifests) {
789     my $nailed = nailed($mf);
790     link $mf, $nailed or die "$self: preserve (link) $mf as $nailed: $!\n";
791     uninstall1($mf,0);
792   }
793   unaltcargolock(0);
794 }
795
796 while (@ARGV && $ARGV[0] =~ m/^-/) {
797   $_ = shift @ARGV;
798   last if m{^--$};
799   if (m{^-[^-]}) {
800     while (m{^-.}) {
801       if (s{^-v}{-}) {
802         $verbose++;
803       } elsif (s{^-q}{-}) {
804         $verbose=0;
805       } elsif (s{^-n}{-}) {
806         $noact++;
807       } elsif (s{^-D}{-}) {
808         $dump++;
809       } elsif (s{^-T(.+)}{-}s) {
810         $target = $1;
811       } elsif (s{^-([uU])}{-}) {
812         $cargo_lock_update= $1=~m/[a-z]/;
813       } elsif (s{^-([mM])}{-}) {
814         $cargo_manifest_args= $1=~m/[a-z]/;
815       } elsif (s{^-([tT])}{-}) {
816         $cargo_target_arg= $1=~m/[a-z]/;
817       } else {
818         die "$self: unknown short option(s) $_\n";
819       }
820     }
821   } elsif (s{^--target=}{}) {
822     $target = $_;
823   } elsif (m{^--(no-)?cargo-lock-update}) {
824     $cargo_lock_update= !!$1;
825   } elsif (m{^--(no-)?cargo-manifest-args}) {
826     $cargo_manifest_args= !!$1;
827   } elsif (m{^--(no-)?cargo-target-arg}) {
828     $cargo_target_arg= !!$1;
829   } elsif (m{^--(on|off)line$}) {
830     $online = $1 eq 'on';
831   } else {
832     die "$self: unknown long option $_\n";
833   }
834 }
835
836 die "$self: need command to run\n" unless @ARGV || $noact;
837
838 takelock();
839 readnail();
840 consider_alt_cargo_lock();
841 consider_oot();
842 readorigs();
843 calculate();
844 addargs();
845 our @display_cmd = @ARGV;
846 oot_massage_cmdline();
847 setenvs();
848
849 if ($dump) {
850   eval '
851     use Data::Dumper;
852     print STDERR Dumper(\%manifests) if $dump>=2;
853     print STDERR Dumper(\%packagemap, \@ARGV,
854                         { src_absdir => $src_absdir,
855                           worksphere => $worksphere,
856                           subdir => $subdir,
857                           oot_dir => $oot_dir,
858                           oot_absdir => $oot_absdir,
859                           build_absdir => $build_absdir });
860   ' or die $@;
861 }
862
863 exit 0 if $noact;
864
865 $want_uninstall = 1;
866 makebackups();
867 install();
868
869 printf STDERR "$self: nailed (%s manifests, %s packages)%s\n",
870   (scalar keys %manifests), (scalar keys %packagemap),
871   (defined($alt_cargo_lock) and ", using `$alt_cargo_lock'")
872   if $verbose;
873
874 print STDERR "$self: invoking: @display_cmd\n" if $verbose;
875 my $estatus = invoke();
876
877 cargo_lock_update_after();
878
879 uninstall();
880 $want_uninstall = 0;
881
882 print STDERR "$self: unnailed.  status $estatus.\n" if $verbose;
883
884 exit $estatus;