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