2 # nailing-cargo: wrapper to use unpublished local crates
3 # SPDX-License-Identifier: AGPL-3.0-or-later
12 RPI => 'arm-unknown-linux-gnueabihf',
16 $self = $0; $self =~ s{^.*/(?=.)}{};
18 while ($deref =~ m{^/}) {
19 my $link = readlink $deref;
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";
28 last if $link !~ m{^/};
33 use Fcntl qw(LOCK_EX);
35 use TOML::Tiny::Faithful;
37 our $src_absdir = getcwd() // die "$self: getcwd failed: $!\n";
39 our $worksphere = $src_absdir;
40 $worksphere =~ s{/([^/]+)$}{}
41 or die "$self: cwd \`$worksphere' unsupported!\n";
42 our $subdir = $1; # leafname
44 our $lockfile = "../.nailing-cargo.lock";
47 our $cargo_lock_update;
48 our $cargo_manifest_args;
49 our $cargo_target_arg=1;
58 sub read_or_enoent ($) {
60 if (!open R, '<', $fn) {
61 return undef if $!==ENOENT;
62 die "$self: open $fn: $!\n";
65 my ($r) = <R> // die "$self: read $fn: $!\n";
69 sub stat_exists ($$) {
71 if (stat $fn) { return 1; }
72 $!==ENOENT or die "$self: stat $what: $fn: $!\n";
76 sub toml_or_enoent ($$) {
78 my $toml = read_or_enoent($f) // return;
79 print STDERR "Read TOML from $f\n" if $dump;
80 my ($v,$e) = from_toml($toml);
83 die "$self: parse TOML: $what: $f: $e\n";
85 die "$e ?" if length $e;
91 my $toml = toml_or_enoent($f, "config file");
92 push @configs, $toml if defined $toml;
96 my $dotfile = ".nailing-cargo.toml";
97 load1config("../Nailing-Cargo.toml");
98 load1config($dotfile);
99 load1config("$ENV{HOME}/$dotfile") if defined $ENV{HOME};
100 load1config("/etc/nailing-cargo/cfg.toml");
103 sub unlink_or_enoent ($) { unlink $_[0] or $!==ENOENT; }
107 "@$x[0..5]" eq "@$y[0..5]";
112 open LOCK, ">", $lockfile or die "$self: open/create $lockfile: $!\n";
113 flock LOCK, LOCK_EX or die "$self: lock $lockfile: $!\n";
114 my @fstat = stat LOCK or die "$self: fstat: $!\n";
115 my @stat = stat $lockfile;
117 next if $! == ENOENT;
118 die "$self: stat $lockfile: $!\n";
120 last if same_file(\@fstat,\@stat);
124 unlink $lockfile or die "$self: removing lockfile: $!\n";
132 die "$self: config key \`@_': $m\n";
136 foreach my $cfg (@configs) {
139 last unless defined $v;
140 ref($v) eq 'HASH' or badcfg @_, "parent key \`$k' is not a hash";
143 return $v if defined $v;
151 my $got = ref($v) || 'scalar';
152 return $v if !defined($v) || $got eq $exp;
153 badcfg @_, "found \L$got\E, expected \L$exp\E";
154 # ^ toml doesn't make refs to scalars, so this is unambiguous
159 (cfge $exp, @_) // badcfg @_, "missing";
162 sub cfgs { cfge 'scalar', @_ }
163 sub cfgsn { cfgn 'scalar', @_ }
167 return $v if !defined($v) || Types::Serialiser::is_bool $v;
168 badcfg @_, "expected boolean";
172 my $l = cfge 'ARRAY', @_;
173 foreach my $x (@$l) {
174 !ref $x or badcfg @_, "list contains non-scalar element";
180 my $nailfile = "../Cargo.nail";
181 open N, '<', $nailfile or die "$self: open $nailfile: $!\n";
183 my $toml = <N> // die "$self: read $nailfile: $!";
185 if ($toml !~ m{^\s*\[/}m &&
186 $toml !~ m{^[^\n\#]*\=}m &&
187 # old non-toml syntax
188 $toml =~ s{^[ \t]*([-_0-9a-z]+)[ \t]+(\S+)[ \t]*$}{$1 = \"$2\"}mig) {
189 $toml =~ s{^}{[packages\]\n};
191 $toml =~ s{^[ \t]*\-[ \t]*\=[ \t]*(\"[-_0-9a-z]+\"\n?)$}{
194 $toml = "subdirs = [\n".(join '', map { "$_\n" } @sd)."]\n".$toml;
198 ($nail,$e) = from_toml($toml);
199 if (!defined $nail) {
202 print STDERR "$self: $nailfile transformed into TOML:\n$toml\n";
205 die "$self: parse $nailfile: $e\n";
207 die "$e ?" if length $e;
209 $nail->{subdirs} //= [ ];
211 if (!ref $nail->{subdirs}) {
214 map { s/^\s+//; s/\s+$//; $_; }
220 unshift @configs, $nail;
223 our @alt_cargo_lock_stat;
225 sub consider_alt_cargo_lock () {
226 my @ck = qw(alt_cargo_lock);
227 # User should *either* have Cargo.lock in .gitignore,
228 # or expect to commit Cargo.lock.example ($alt_cargo_lock)
230 $alt_cargo_lock = (cfg_uc @ck);
233 if (defined($alt_cargo_lock) && ref($alt_cargo_lock) eq 'HASH') {
234 $force = cfg_bool qw(alt_cargo_lock force);
235 my @ck = qw(alt_cargo_lock file);
236 $alt_cargo_lock = cfg_uc @ck;
238 $alt_cargo_lock //= Types::Serialiser::true;
240 if (Types::Serialiser::is_bool $alt_cargo_lock) {
241 if (!$alt_cargo_lock) { $alt_cargo_lock = undef; return; }
242 $alt_cargo_lock = 'Cargo.lock.example';
245 if (ref($alt_cargo_lock) || $alt_cargo_lock =~ m{/}) {
246 badcfg @ck, "expected boolean, or leafname";
249 if (!stat_exists $alt_cargo_lock, "alt_cargo_lock") {
250 $alt_cargo_lock = undef unless $force;
254 @alt_cargo_lock_stat = stat _;
257 our $oot_dir; # oot.dir or "Build"
259 sub consider_oot () {
260 $oot_dir = cfgs qw(oot dir);
261 my $use = cfgs qw(oot use);
262 unless (defined($oot_dir) || defined($use)) {
263 die "$self: specified --cargo-lock-update but not out-of-tree build!\n"
264 if $cargo_lock_update;
265 $cargo_lock_update=0;
268 if ($use eq 'disable') {
272 $oot_dir //= 'Build';
278 sub read_manifest ($) {
280 my $manifest = "../$subdir/Cargo.toml";
281 print STDERR "$self: reading $manifest...\n" if $verbose>=4;
282 if (defined $manifests{$manifest}) {
284 "$self: warning: $subdir: specified more than once!\n";
287 foreach my $try ("$manifest.unnailed", "$manifest") {
288 my $toml = toml_or_enoent($try, "package manifest") // next;
289 my $p = $toml->{package}{name};
292 "$self: warning: $subdir: missing package.name in $try, ignoring\n";
295 $manifests{$manifest} = $toml;
302 foreach my $p (keys %{ $nail->{packages} }) {
303 my $v = $nail->{packages}{$p};
304 my $subdir = ref($v) ? $v->{subdir} : $v;
305 my $gotpackage = read_manifest($subdir) // '<nothing!>';
306 if ($gotpackage ne $p) {
308 "$self: warning: honouring Cargo.nail packages.$subdir=$p even though $subdir contains package $gotpackage!\n";
310 die if defined $packagemap{$p};
311 $packagemap{$p} = $subdir;
313 foreach my $subdir (@{ $nail->{subdirs} }) {
314 my $gotpackage = read_manifest($subdir);
315 if (!defined $gotpackage) {
317 "$self: warning: ignoring subdir $subdir which has no Cargo.toml\n";
320 $packagemap{$gotpackage} //= $subdir;
325 foreach my $p (sort keys %packagemap) {
326 print STDERR "$self: package $p in $packagemap{$p}\n" if $verbose>=2;
328 foreach my $mf (keys %manifests) {
329 my $toml = $manifests{$mf};
330 foreach my $k (qw(dependencies build-dependencies dev-dependencies)) {
331 my $deps = $toml->{$k};
333 foreach my $p (keys %packagemap) {
334 my $info = $deps->{$p};
335 next unless defined $info;
336 $deps->{$p} = $info = { } unless ref $info;
337 delete $info->{version};
338 $info->{path} = $worksphere.'/'.$packagemap{$p};
341 my $nailing = "$mf.nailing~";
342 unlink_or_enoent $nailing or die "$self: remove old $nailing: $!\n";
343 open N, '>', $nailing or die "$self: create new $nailing: $!\n";
344 print N to_toml($toml) or die "$self: write new $nailing: $!\n";
345 close N or die "$self: close new $nailing: $!\n";
350 $online //= cfg_bool qw(misc online);
352 if ($cargo_subcmd =~ m/^(?:generate-lockfile|update)$/) {
353 $cargo_lock_update //= 1;
356 if ($cargo_subcmd =~ m/^(?:fetch)$/) {
360 $cargo_lock_update //= 0;
361 $cargo_manifest_args //=
362 (defined $oot_dir) && !$cargo_lock_update;
366 if ($cargo_manifest_args) {
367 push @ARGV, "--manifest-path=${src_absdir}/Cargo.toml",
369 push @ARGV, qw(--target-dir=target) if $cargo_target_arg;
372 if (defined $target) {
373 if ($target =~ m{^[A-Z]}) {
374 $target = (cfgs 'arch', $target) // $archmap{$target}
375 // die "$self: --target=$target alias specified; not in cfg or map\n";
377 push @ARGV, "--target=$target";
380 push @ARGV, "--offline" unless $online;
384 our $build_absdir; # .../Build/<subdir>
386 sub oot_massage_cmdline () {
387 return unless defined $oot_dir;
389 my $use = cfgs qw(oot use);
390 $oot_absdir = ($oot_dir !~ m{^/} ? "$worksphere/" : ""). $oot_dir;
391 $build_absdir = "$oot_absdir/$subdir";
395 if (!$cargo_lock_update) {
396 push @xargs, $build_absdir;
397 ($pre, $post) = ('cd "$1"; shift; ', '');
399 push @xargs, $oot_absdir, $subdir, $src_absdir;
402 mkdir -p -- "$1"; cd "$1"; shift;
403 cp -- "$1"/Cargo.toml
405 $pre .= <<'ENDLK' if stat_exists 'Cargo.lock', 'working cargo lockfile';
413 mkdir -p src; >src/lib.rs; >build.rs
416 rm -r src Cargo.toml build.rs;
419 my $addpath = (cfg_uc qw(oot path_add)) //
420 $use eq 'really' ? Types::Serialiser::true : Types::Serialiser::false;
422 !Types::Serialiser::is_bool $addpath ? $addpath :
423 $addpath ? '$HOME/.cargo/bin' :
425 if (defined $addpath) {
427 PATH=$addpath:\${PATH-/usr/local/bin:/bin:/usr/bin};
431 $pre =~ s/^\s+//mg; $pre =~ s/\s+/ /g;
432 $post =~ s/^\s+//mg; $post =~ s/\s+/ /g;
434 my $getuser = sub { cfgsn qw(oot user) };
436 my $xe = $verbose >= 2 ? 'xe' : 'e';
439 @command = (@_, 'sh',"-${xe}c",$pre.'exec "$@"','--',@xargs);
441 @command = (@_, 'sh',"-${xe}c",$pre.'"$@"; '.$post,'--',@xargs);
443 push @command, @ARGV;
445 my $command_sh = sub {
446 my $quoted = join ' ', map {
451 @command = @_, "set -${xe}; $pre $quoted; $post";
453 print STDERR "$self: out-of-tree, building in: \`$build_absdir'\n"
455 if ($use eq 'really') {
456 my $user = $getuser->();
457 my @pw = getpwnam $user or die "$self: oot.user \`$user' lookup failed\n";
458 my $homedir = $pw[7];
459 $sh_ec->('really','-u',$user,'env',"HOME=$homedir");
460 print STDERR "$self: using really to run as user \`$user'\n" if $verbose;
461 } elsif ($use eq 'ssh') {
462 my $user = $getuser->();
463 $user .= '@localhost' unless $user =~ m/\@/;
464 $command_sh->('ssh',$user);
465 print STDERR "$self: using ssh to run as \`$user'\n" if $verbose;
466 } elsif ($use eq 'command_args') {
467 my @c = cfgn_list qw(oot command);
469 print STDERR "$self: out-of-tree, adverbial command: @c\n" if $verbose;
470 } elsif ($use eq 'command_sh') {
471 my @c = cfgn_list qw(oot command);
473 print STDERR "$self: out-of-tree, ssh'ish command: @c\n" if $verbose;
474 } elsif ($use eq 'null') {
477 die "$self: oot.use mode $use not recognised\n";
484 $ENV{NAILINGCARGO_WORKSPHERE} = $worksphere;
485 $ENV{NAILINGCARGO_MANIFEST_DIR} = $src_absdir;
486 $ENV{NAILINGCARGO_BUILDSPHERE} = $oot_absdir;
487 delete $ENV{NAILINGCARGO_BUILDSPHERE} unless $oot_absdir;
488 $ENV{NAILINGCARGO_BUILD_DIR} = $build_absdir // $src_absdir;
494 if ($want_uninstall) {
496 foreach my $mf (keys %manifests) {
497 eval { uninstall1($mf,1); 1; } or warn "$@";
499 eval { unaltcargolock(1); 1; } or warn "$@";
503 our $cleanup_cargo_lock;
505 foreach my $mf (keys %manifests) {
506 link "$mf", "$mf.unnailed" or $!==EEXIST
507 or die "$self: make backup link $mf.unnailed: $!\n";
510 if (defined($alt_cargo_lock)) {
511 if (@alt_cargo_lock_stat) {
512 print STDERR "$self: using alt_cargo_lock `$alt_cargo_lock'..."
514 if (link $alt_cargo_lock, 'Cargo.lock') {
515 print STDERR " linked\n" if $verbose>=3;
516 } elsif ($! != EEXIST) {
517 print STDERR "\n" if $verbose>=3;
518 die "$self: make \`Cargo.lock' available as \`$alt_cargo_lock': $!\n";
520 print STDERR "checking quality." if $verbose>=3;
521 my @lock_stat = stat 'Cargo.lock'
522 or die "$self: stat Cargo.lock (for alt check: $!\n";
523 same_file(\@alt_cargo_lock_stat, \@lock_stat)
525 "$self: \`Cargo.lock' and alt file \`$alt_cargo_lock' both exist and are not the same file!\n";
527 $cleanup_cargo_lock = 1;
529 $cleanup_cargo_lock = 1;
530 # If Cargo.lock exists and alt doesn't, that means either
531 # that a previous run was interrupted, or that the user has
539 my $nailed = "$mf.nailed~"; $nailed =~ s{/([^/]+)$}{/.$1} or die;
544 my @our_unfound_stab = stat_exists('Cargo.toml', 'local Cargo.toml')
546 foreach my $mf (keys %manifests) {
547 if (@our_unfound_stab) {
548 if (stat_exists $mf, "manifest in to-be-nailed directory") {
549 my @mf_stab = stat _ ;
550 if ("@mf_stab[0..1]" eq "@our_unfound_stab[0..1]") {
551 @our_unfound_stab = ();
556 my $nailing = "$mf.nailing~";
557 my $nailed = nailed($mf);
560 if (open NN, '<', $nailed) {
561 $diff = compare($nailing, \*NN);
562 die "$self: compare $nailing and $nailed: $!" if $diff<0;
564 $!==ENOENT or die "$self: check previous $nailed: $!\n";
574 rename $use, $mf or die "$self: install nailed $use: $!\n";
575 unlink_or_enoent $rm or die "$self: remove old $rm: $!\n";
576 print STDERR "$self: nailed $mf\n" if $verbose>=3;
579 if (@our_unfound_stab) {
581 "$self: *WARNING* cwd is not in Cargo.nail thbough it has Cargo.toml!\n";
586 my $r = system @ARGV;
590 print STDERR "$self: could not execute $ARGV[0]: $!\n";
592 } elsif ($r & 0xff00) {
593 print STDERR "$self: $ARGV[0] failed (exit status $r)\n";
596 print STDERR "$self: $ARGV[0] died due to signal! (wait status $r)\n";
601 sub cargo_lock_update_after () {
602 if ($cargo_lock_update) {
603 # avoids importing File::Copy and the error handling is about as good
605 my $r= system qw(cp --), "$build_absdir/Cargo.lock", "Cargo.lock";
606 die "$self: run cp: $! $?" if $r<0 || $r & 0xff;
607 die "$self: failed to update local Cargo.lock (wait status $r)\n" if $r;
611 sub uninstall1 ($$) {
612 my ($mf, $enoentok) = @_;
613 my $unnailed = "$mf.unnailed";
614 rename $unnailed, $mf or ($enoentok && $!==ENOENT)
615 or die "$self: failed to restore: rename $unnailed back to $mf: $!\n";
618 sub unaltcargolock ($) {
620 return unless $cleanup_cargo_lock;
621 die 'internal error!' unless defined $alt_cargo_lock;
623 # we ignore $enoentok because we don't know if one was supposed to
626 rename('Cargo.lock', $alt_cargo_lock) or $!==ENOENT or die
627 "$self: cleanup: rename possibly-updated \`Cargo.lock' to \`$alt_cargo_lock': $!\n";
629 unlink 'Cargo.lock' or $!==ENOENT or die
630 "$self: cleanup: remove \`Cargo.lock' in favour of \`$alt_cargo_lock': $!\n";
631 # ^ this also helps clean up the stupid rename() corner case
635 foreach my $mf (keys %manifests) {
636 my $nailed = nailed($mf);
637 link $mf, $nailed or die "$self: preserve (link) $mf as $nailed: $!\n";
646 # Loop exit condition:
649 # $is_cargo==1 <cargo-command> <cargo-opts> [--] <subcmd>...
650 # $is_cargo==0 <build-command>...
653 @ARGV or die "$self: need cargo subcommand\n";
658 my $not_a_nailing_opt = sub { # usage 1
659 unshift @ARGV, $orgopt;
660 unshift @ARGV, 'cargo';
662 no warnings qw(exiting);
665 $not_a_nailing_opt->() unless m{^-};
666 $not_a_nailing_opt->() if $_ eq '--';
668 if ($_ eq '---') { # usage 2 or 3
669 die "$self: --- must be followed by build command\n" unless @ARGV;
670 if ($ARGV[0] eq '--') { # usage 3
673 } elsif (grep { $_ eq '--' } @ARGV) { # usage 2
675 } elsif ($ARGV[0] =~ m{[^/]*cargo[^/]*$}) { # usage 2
686 } elsif (s{^-q}{-}) {
688 } elsif (s{^-n}{-}) {
690 } elsif (s{^-D}{-}) {
692 } elsif (s{^-A(.+)}{-}s) {
694 } elsif (s{^-([uU])}{-}) {
695 $cargo_lock_update= $1=~m/[a-z]/;
696 } elsif (s{^-([mM])}{-}) {
697 $cargo_manifest_args= $1=~m/[a-z]/;
698 } elsif (s{^-([tT])}{-}) {
699 $cargo_target_arg= $1=~m/[a-z]/;
700 } elsif (s{^-([oO])}{-}) {
701 $online= $1=~m/[a-z]/;
703 die "$self: unknown short option(s) $_\n" unless $_ eq $orgopt;
704 $not_a_nailing_opt->();
707 } elsif (s{^--(?:target|arch)=}{}) {
709 } elsif (m{^--(no-)?cargo-lock-update}) {
710 $cargo_lock_update= !!$1;
711 } elsif (m{^--(no-)?cargo-manifest-args}) {
712 $cargo_manifest_args= !!$1;
713 } elsif (m{^--(no-)?cargo-target-dir-arg}) {
714 $cargo_target_arg= !!$1;
715 } elsif (m{^--(on|off)line$}) {
716 $online = $1 eq 'on';
718 $not_a_nailing_opt->();
726 my @cargo_and_opts = shift @ARGV;
727 while (defined($_ = shift @ARGV)) {
728 if (!m{^-}) { unshift @ARGV, $_; last; }
729 if ($_ eq '--') { last; }
730 push @cargo_and_opts, $_;
732 @ARGV || die "$self: need cargo subcommand\n";
733 $cargo_subcmd = $ARGV[0];
734 unshift @ARGV, @cargo_and_opts;
744 consider_alt_cargo_lock();
749 our @display_cmd = @ARGV;
750 oot_massage_cmdline();
756 print STDERR Dumper(\%manifests) if $dump>=2;
757 print STDERR Dumper(\%packagemap, \@ARGV,
758 { src_absdir => $src_absdir,
759 worksphere => $worksphere,
762 oot_absdir => $oot_absdir,
763 build_absdir => $build_absdir });
773 printf STDERR "$self: nailed (%s manifests, %s packages)%s\n",
774 (scalar keys %manifests), (scalar keys %packagemap),
775 (defined($alt_cargo_lock) and ", using `$alt_cargo_lock'")
778 print STDERR "$self: invoking: @display_cmd\n" if $verbose;
779 my $estatus = invoke();
781 cargo_lock_update_after();
786 print STDERR "$self: unnailed. status $estatus.\n" if $verbose;