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