X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?a=blobdiff_plain;f=nailing-cargo;h=c9ee071e3f578305207da6ba4440408434ff54cf;hb=refs%2Fheads%2Fmain;hp=d14f1943c5f788ea3673cfeb8cf7654e7b9bcbdf;hpb=b56effed2c01e879d43d9534f5af7ce31da9dcfb;p=nailing-cargo.git diff --git a/nailing-cargo b/nailing-cargo index d14f194..c9ee071 100755 --- a/nailing-cargo +++ b/nailing-cargo @@ -1,176 +1,65 @@ #!/usr/bin/perl -w +# nailing-cargo: wrapper to use unpublished local crates +# SPDX-License-Identifier: AGPL-3.0-or-later +our $usage = <<'END'; + +usages: + + nailing-cargo [--] ... + nailing-cargo --- [--] ... + nailing-cargo --- [--] ... + +options: + + -v Increase verbosity. (Default is 1) + -q Set verbosity to 0 + -D Increase amount of debugging dump. + -n "No action": stop after writing Cargo.toml.nailing~ + everywhere, and do not run any build command + + -c Do add cargo command line options } default is add if + -C Do not add cargo command line options } command is cargo + + -o --online -O --offline + -u --cargo-lock-update -U --no-cargo-lock-update + + -T --target= Specify target architecture + -h --help Print this message + --doc --man --manual Display complete manual (in w3m) + --leave-nailed Leave the nailed Cargo.toml in place + -E | --edits-sources Allow source edits (repeat: file creation) + -f | --force Override some warnings + --linkfarm[=no|shallow|git|full] (default varies, usually "no") + --just-linkfarm | --clean-linkfarm | --keep-linkfarm (default is keep) + --[no-]preclean-build[=no|src|full] (default is no) + --just-run Run the command, don't do cargo stuff + --no-nail Do not nail, just run the command. + --no-cargo-lock-manip Do not manipulate Cargo.lock. + --no-concurrency-lock Do not take the concurrency lock. + + -s Treat command as `cargo ` + --subcommand-props=,... Override command props (see docs) -# nailing-cargo: wrapper to use unpublished local crates -# -# Copyright (C) 2019-2020 Ian Jackson -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -# example usages: -# ../nailing-cargo/nailing-cargo make -# ../nailing-cargo/nailing-cargo cargo build - -# Why do we need this ? -# -# https://github.com/rust-lang/cargo/issues/6713 -# https://stackoverflow.com/questions/33025887/how-to-use-a-local-unpublished-crate -# https://github.com/rust-lang/cargo/issues/1481 - -# Options: -# -v Increase verbosity. Default is 1. -# -q Set verbosity ot 0. -# -D Increase amount of debugging dump. -# -# -n "No action": stop after writing Cargo.toml.nailing~ -# everywhere, and do not run any build command. -# -# -T --target= -# Specify target architecture. If starts with a -# capital ascii letter, is an alias for some other arch -# looked up in Cargo.nail and then in the builtin list: -# RPI arm-unknown-linux-gnueabihf -# Translates to a --target= option to the ultimate command, -# unless that is a cargo subcommand which would reject it. -# -# -u | --cargo-lock-update -# -U | --no-cargo-lock-update -# Arranges to do a dance to allow Cargo.lock (or -# alternative) to be updated in the source directory. -# -# The Cargo.lock and Cargo.toml are copied to the build -# directory along with a skeleton just big enough to fool -# cargo. After cargo has run, the resulting Cargo.lock is -# copied back to the source tree. -# -# Makes no sense with in-tree builds. -# -# Default is no update unless the ultimate command is a -# cargo subcommand which we know needs it. -# -# -m | --cargo-manifest-args -# -M | --no-cargo-manifest-args -# Controls whether we add cargo command line options -# relating to finding Cargo.toml, to the command to -# run. -# -# Default is true if we are doing an out-of- tree build, -# unless we are updating the Cargo.lock (in which case the -# only relevant files are to be found in the build directory). -# -# The arguments are -# --manifest-path= -# --locked -# --target-dir=target -# -# -T | --no-cargo-target-arg -# -t | --cargo-target-arg -# Suppress --target (or un-suppress it). -# Only useful with -m. -# -# Cargo.nail: -# -# # Adds each to the list of directories whose -# # Cargo.toml is to be nailed, and also arranges to use -# # the package found there for other directories if -# # no other source of that package is evident in Cargo.nail. -# subdirs = [ -# "" -# ... -# ] -# or: -# subdirs = """ -# "" -# ... -# """ -# -# # Adds to the list of directories whose Cargo.toml -# # is to be nailed, and overrides any other nailing for -# [packages] -# = -# = { = ... } -# -# To control use of alternative Cargo.lock filename: -# [alt_cargolock] -# -# file = true # equivalent to "Cargo.lock.example" -# file = false # disables this feature -# file = "" -# -# force = false # default, uses alt file only if it already exists -# force = true # always uses alt file; creation would make Cargo.lock -# -# (you can also specify just alt_cargo_lock instead of alt_cargo_lock.file) -# -# To enable out of tree builds: -# [oot] -# dir = "" # default is Build, if use is specified -# and then -# use = "really" -# user = "" -# or -# use = "ssh" -# user = "@host" # NB must still share a filesystem! -# or -# use = "command_args" -# command = ["", "", "nice"] -# or -# use = "command_sh" -# command = ["", "", "sh -c"] -# or -# use = "null" -# -# Limitations: -# -# Always dirties everyone's Cargo.toml, but tries to put them -# back (if not, running it again should fix it). Cannnot be -# fixed without changes to cargo. -# -# Out of tree builds require a unified filesystem view: eg, -# different users on the same host, NFS, or something. This -# could be improved. -# -# Alternative Cargo.lock file must currently be a leafname. -# I think this just involves review to check other values work. -# -# Alternative Cargo.lock file must be on smae filesystem. -# This is not so easy; we would want the existing algorithm but -# a fallback for this case. -# -# Cargo.nail unconditionally looked for in .. -# Ideally should be configurable, and also perhaps be able -# to combine multiple Cargo.nail files ? -# -# Env vars we pass to the command: -# NAILINGCARGO_WORKSPHERE absolute path of invocation .. -# NAILINGCARGO_MANIFEST_DIR absolute path of invocation . -# NAILINGCARGO_BUILDSPHERE only if out of tree: abs parent of build dir -# NAILINGCARGO_BUILD_DIR absolute path of build dir (even if = src) - +END our $self; use strict; use POSIX; use Types::Serialiser; +use File::Glob qw(bsd_glob GLOB_ERR GLOB_BRACE GLOB_NOMAGIC); +use Cwd qw(realpath); +our $base_path; our %archmap = ( RPI => 'arm-unknown-linux-gnueabihf', + WASM => 'wasm32-unknown-unknown', ); BEGIN { $self = $0; $self =~ s{^.*/(?=.)}{}; my $deref = $0; + our $base_path; while ($deref =~ m{^/}) { my $link = readlink $deref; if (!defined $link) { @@ -178,6 +67,7 @@ BEGIN { or die "$self: checking our script location $deref: $!\n"; $deref =~ s{/[^/]+$}{} or die "$self: unexpected script path: $deref\n"; + $base_path = $deref; unshift @INC, $deref."/TOML-Tiny/lib"; last; } @@ -199,16 +89,68 @@ our $subdir = $1; # leafname our $lockfile = "../.nailing-cargo.lock"; -our $cargo_lock_update; -our $cargo_manifest_args; -our $cargo_target_arg=1; +our @args_preface; +our $cargo_subcmd; +our $command_is_cargo; our $alt_cargo_lock; +our $cargo_lock_update; +our $pass_options; +our $online; +our $just_linkfarm; +our $leave_nailed; +our $oot_clean; +our $oot_preclean; +our $do_nail=1; +our $do_cargo_lock=1; +our $do_lock=1; +our $linkfarm_depth; + +# +our %subcmd_props = ( +# build (default) =>[qw( )], + fetch =>[qw( online !target-dir )], + fmt =>[qw( !locked !target !offline !target-dir edits )], +'generate-lockfile'=>[qw( lock-update !target !target-dir )], + init =>[qw( creates )], + metadata =>[qw( !target-dir )], + miri =>[qw( !locked !offline linkfarm-shallow )], + publish =>[qw( !offline linkfarm-gitclean )], + update =>[qw( lock-update !target online )], + upgrades =>[qw( !locked !target-dir )], + ); + +our @subcmd_xprops = qw(!manifest-path); our @configs; our $verbose=1; +our $force=0; +our $forced=0; our ($noact,$dump); our $target; +sub print_usage () { + print $usage or die $!; + exit 0; +} + +sub show_manual () { + my $manual = ($base_path // '.').'/README.md'; + stat $manual or die "$self: manual not found at $manual: $!\n";; + exec 'sh','-ec', 'pandoc -- "$1" 2>&1 | w3m -T text/html', '--', $manual; + die "$self: exec sh failed: $!"; +} + +sub forceable_warning ($) { + my ($m) = @_; + print STDERR "$self: *WARNING*: $m\n"; + if ($force) { + print STDERR "$self: continuing because of --force...\n" unless $forced++; + return; + } else { + die "$self: Stopping due to warning (override with -f | --force)\n"; + } +} + sub read_or_enoent ($) { my ($fn) = @_; if (!open R, '<', $fn) { @@ -227,9 +169,16 @@ sub stat_exists ($$) { return 0; } +sub subcmd_p ($) { + print STDERR " subcmd_p ".(join ' ', keys %$cargo_subcmd)." | @_\n" + if $dump; + $cargo_subcmd->{$_[0]} +} + sub toml_or_enoent ($$) { my ($f,$what) = @_; my $toml = read_or_enoent($f) // return; + print STDERR "Read TOML from $f\n" if $dump; my ($v,$e) = from_toml($toml); if (!defined $v) { chomp $e; @@ -246,19 +195,11 @@ sub load1config ($) { } sub loadconfigs () { - my $cfgleaf = ".nailing-cargo-cfg.toml"; + my $dotfile = ".nailing-cargo.toml"; + load1config("../Nailing-Cargo.toml"); + load1config($dotfile); + load1config("$ENV{HOME}/$dotfile") if defined $ENV{HOME}; load1config("/etc/nailing-cargo/cfg.toml"); - load1config("$worksphere/$cfgleaf"); - load1config("$ENV{HOME}/$cfgleaf") if defined $ENV{HOME}; -} - -sub getcfg ($$) { - my ($k, $def) = @_; - foreach my $cfg (@configs) { - my $v = $cfg->{$k}; - return $v if defined $v; - } - return $def; } sub unlink_or_enoent ($) { unlink $_[0] or $!==ENOENT; } @@ -269,6 +210,8 @@ sub same_file ($$) { } sub takelock () { + return unless $do_lock; + for (;;) { open LOCK, ">", $lockfile or die "$self: open/create $lockfile: $!\n"; flock LOCK, LOCK_EX or die "$self: lock $lockfile: $!\n"; @@ -294,13 +237,16 @@ sub badcfg { } sub cfg_uc { - my $v = $nail; - foreach my $k (@_) { - last unless defined $v; - ref($v) eq 'HASH' or badcfg @_, "parent key \`$k' is not a hash"; - $v = $v->{$k}; + foreach my $cfg (@configs) { + my $v = $cfg; + foreach my $k (@_) { + last unless defined $v; + ref($v) eq 'HASH' or badcfg @_, "parent key \`$k' is not a hash"; + $v = $v->{$k}; + } + return $v if defined $v; } - return $v; + return undef; } sub cfge { @@ -374,6 +320,26 @@ sub readnail () { $nail->{subdirs} ]; } + + unshift @configs, $nail; +} + +sub get_dependency_tables ($) { + my ($toml) = @_; + my @keys = qw(dependencies build-dependencies dev-dependencies); + my @r; + my $process = sub { + my ($node) = @_; + foreach my $k (@keys) { + my $deps = $node->{$k}; + push @r, $deps if $deps; + } + }; + $process->($toml); + foreach my $target_node (values %{ $toml->{target} // { } }) { + $process->($target_node); + } + @r; } our @alt_cargo_lock_stat; @@ -383,6 +349,8 @@ sub consider_alt_cargo_lock () { # User should *either* have Cargo.lock in .gitignore, # or expect to commit Cargo.lock.example ($alt_cargo_lock) + return unless $do_cargo_lock; + $alt_cargo_lock = (cfg_uc @ck); my $force = 0; @@ -411,83 +379,184 @@ sub consider_alt_cargo_lock () { } our $oot_dir; # oot.dir or "Build" +our $oot_absdir; sub consider_oot () { $oot_dir = cfgs qw(oot dir); my $use = cfgs qw(oot use); - unless (defined($oot_dir) || defined($use)) { - die "$self: specified --cargo-lock-update but not out-of-tree build!\n" - if $cargo_lock_update; - $cargo_lock_update=0; + unless (defined($oot_dir) || defined($use) || + defined(cfg_uc qw(oot user))) { return; } + if (($use//'') eq 'disable') { + $oot_dir = undef; + return; + } + $oot_clean //= cfg_bool qw(oot clean); $oot_dir //= 'Build'; + $oot_absdir = ($oot_dir !~ m{^/} ? "$worksphere/" : ""). $oot_dir; } our %manifests; our %packagemap; +our %workspaces; +our @queued_paths; -sub read_manifest ($) { - my ($subdir) = @_; +sub read_manifest ($$$) { + my ($subdir, $org_subdir, $why) = @_; my $manifest = "../$subdir/Cargo.toml"; print STDERR "$self: reading $manifest...\n" if $verbose>=4; if (defined $manifests{$manifest}) { print STDERR - "$self: warning: $subdir: specified more than once!\n"; + "$self: warning: $subdir: specified more than once!". + " (ignoring $why)\n"; return undef; } foreach my $try ("$manifest.unnailed", "$manifest") { - my $toml = toml_or_enoent($try, "package manifest") // next; + my $toml = toml_or_enoent($try, "manifest, in $why") // next; + my $ws = $toml->{workspace}; + if ($ws) { + queue_workspace_members($subdir, $org_subdir, $ws, "$subdir, $why"); + } my $p = $toml->{package}{name}; - if (!defined $p) { + if (!defined $p and !defined $ws) { print STDERR - "$self: warning: $subdir: missing package.name in $try, ignoring\n"; + "$self: warning: $subdir, $why: missing package.name in $try, ignoring\n"; next; } - $manifests{$manifest} = $toml; - return $p; + $manifests{$manifest} = [ $toml, $org_subdir ] if $p; + foreach my $dep (get_dependency_tables $toml) { + next unless defined $dep->{path}; + queue_referenced_path($dep->{path}, $org_subdir, + "dependency of $subdir, $why"); + } + return ($p, $ws); } return undef; } +sub queue_workspace_members ($$) { + my ($subdir, $org_subdir, $ws_toml, $what) = @_; + # We need to (more or less) reimplement the cargo workspace + # membership algorithm (see the "workspaces" section of the cargo + # reference). How tiresome. + # + # It's not quite the same for us because we aren't interested in + # whether cargo thinks things are "in the workspace". But we do + # need to do the automatic discover. + + my @include = @{ $ws_toml->{members} // [ ] }; + my $exclude = $ws_toml->{exclude} // [ ]; + + my @exclude = map { + s/[^*?0-9a-zA-Z_]/\\$&/g; + s/\?/./g; + s/\*/.*/g; + } @$exclude; + + foreach my $spec (@include) { + if ($spec =~ m{^/}) { + print STDERR + "$self: warning: absolute workspace member $spec in $what (not nailing, but cargo will probably use it)\n"; + next; + } + my $spec_glob = "../$subdir/$spec"; + my $globflags = GLOB_ERR|GLOB_BRACE|GLOB_NOMAGIC; + foreach my $globent (bsd_glob($spec_glob, $globflags)) { + next if grep { $globent =~ m{^$_$} } @exclude; + queue_referenced_path($globent, $org_subdir, + "member of workspace $what"); + } + } +} + +sub queue_referenced_path ($$$) { + my ($spec_path, $org_subdir, $why) = @_; + open REALPATH, "-|", + qw(realpath), "--relative-to=../$org_subdir", "--", $spec_path + or die "$self: fork/pipe/exec for realpath(1)\n"; + my $rel_path = do { local $/=undef; ; }; + $?=0; $!=0; + my $r = close(REALPATH); + die "$self: reap realpath: $!\n" if $!; + if (!chomp($rel_path) or $?) { + print STDERR + "$self: warning: failed to determine realpath for $spec_path in $org_subdir (exit code $?)\n"; + return; + } + if ($rel_path =~ m{^\.\./} or $rel_path eq '..') { + print STDERR + "$self: warning: $spec_path ($why) points outside $org_subdir, not following so not nailing (although cargo probably will follow it)\n"; + return; + } + + my $q_subdir = "$org_subdir/$rel_path"; + print STDERR "$self: making a note to look at $q_subdir, $why)\n" + if $verbose >= 4; + + push @queued_paths, [ "$q_subdir", $org_subdir, $why ]; +} + sub readorigs () { + # We (and our callees) populate %packagemap and %manifest, so if we + # don't run, they remain empty and nothing is nailed. + return unless $do_nail; + foreach my $p (keys %{ $nail->{packages} }) { my $v = $nail->{packages}{$p}; my $subdir = ref($v) ? $v->{subdir} : $v; - my $gotpackage = read_manifest($subdir) // ''; + my ($gotpackage, $ws) = read_manifest($subdir, $subdir, "from [packages]"); + $gotpackage //= ''; if ($gotpackage ne $p) { print STDERR "$self: warning: honouring Cargo.nail packages.$subdir=$p even though $subdir contains package $gotpackage!\n"; } die if defined $packagemap{$p}; - $packagemap{$p} = $subdir; + $packagemap{$p} = [ $subdir, $subdir ]; } foreach my $subdir (@{ $nail->{subdirs} }) { - my $gotpackage = read_manifest($subdir); + my ($gotpackage,$ws) = read_manifest($subdir, $subdir, "from [subdirs]"); if (!defined $gotpackage) { print STDERR - "$self: warning: ignoring subdir $subdir which has no Cargo.toml\n"; + "$self: warning: ignoring subdir $subdir which has no (suitable) Cargo.toml\n" + unless $ws; next; } - $packagemap{$gotpackage} //= $subdir; + $packagemap{$gotpackage} //= [ $subdir, $subdir ]; + } + while (my ($subdir, $org_subdir, $why) = @{ shift @queued_paths or [] }) { + next if $manifests{"../$subdir/Cargo.toml"}; + my ($gotpackage, $ws) = read_manifest($subdir, $org_subdir, $why); + next unless $gotpackage; + $packagemap{$gotpackage} //= [ $subdir, $org_subdir ]; } } sub calculate () { foreach my $p (sort keys %packagemap) { - print STDERR "$self: package $p in $packagemap{$p}\n" if $verbose>=2; + print STDERR "$self: package $p in $packagemap{$p}[0]\n" if $verbose>=2; } foreach my $mf (keys %manifests) { - my $toml = $manifests{$mf}; - foreach my $k (qw(dependencies build-dependencies dev-dependencies)) { - my $deps = $toml->{$k}; + die "internal error" unless $do_nail; # belt and braces + + my ($toml, $mf_org_subdir) = @{ $manifests{$mf} }; + foreach my $deps (get_dependency_tables $toml) { next unless $deps; - foreach my $p (keys %packagemap) { - my $info = $deps->{$p}; - next unless defined $info; - $deps->{$p} = $info = { } unless ref $info; + foreach my $dep_key (keys %$deps) { + my $info = $deps->{$dep_key}; + my $p = (ref $info ? $info->{package} : undef) // $dep_key; + next unless defined $packagemap{$p}; + next if $packagemap{$p}[1] eq $mf_org_subdir; + $deps->{$dep_key} = $info = { } unless ref $info; # was just version + my $oldpath = $info->{path}; delete $info->{version}; - $info->{path} = $worksphere.'/'.$packagemap{$p}; + my $newpath = $worksphere.'/'.$packagemap{$p}[0]; + print STDERR "in $mf set $p path=$newpath (was ". + ($oldpath // '').")\n" + if $verbose >= 4; + $info->{path} = $newpath; + delete $info->{git}; + delete $info->{branch}; } } my $nailing = "$mf.nailing~"; @@ -499,67 +568,185 @@ sub calculate () { } sub addargs () { - if (@ARGV>=2 && - $ARGV[0] =~ m{\bcargo\b}) { - if ($ARGV[1] =~ m/^(?:generate-lockfile|update)$/) { - $cargo_lock_update //= 1; - $target = undef; + if ($just_linkfarm) { + die "$self: --just-linkfarm but not doing out-of-tree builds!\n" + unless defined $oot_dir; + @ARGV = (); + return; + } + + if (!defined $online) { + $_ = cfg_uc qw(misc online); + if (!defined $_) { + } elsif (Types::Serialiser::is_bool $_) { + $online = $_; + } elsif (ref $_) { + } elsif (m/^a/) { + $online = undef; + } elsif (m/^[1ty]/) { # allow booleanish strings + $online = 1; # for less user frustration + } elsif (m/^[0fn]/) { + $online = 0; + } else { + badcfg qw(misc online), "expected boolean or 'auto', found '$_'"; + } + } + $online //= 1 if subcmd_p('online'); + $online //= 0; + + if (($linkfarm_depth//'') eq 'copy-edit-all') { + $oot_preclean //= 'src'; + if ($oot_preclean !~ m/^(?:src|full)$/) { + forceable_warning + "-EE specified, but also --preclean=no; will probably leave your source tree full of junk"; } } - $cargo_lock_update //= 0; - $cargo_manifest_args //= - (defined $oot_dir) && !$cargo_lock_update; - if ($cargo_manifest_args) { - push @ARGV, "--manifest-path=${src_absdir}/Cargo.toml", - qw(--locked); - push @ARGV, qw(--target-dir=target) if $cargo_target_arg; + if (subcmd_p('linkfarm-gitclean')) { + $linkfarm_depth //= 'git'; + $oot_preclean //= 'src'; } - if (defined $target) { + $cargo_lock_update //= subcmd_p('lock-update'); + $linkfarm_depth //= + subcmd_p('linkfarm-shallow') ? 'shallow' : + $cargo_lock_update ? 'shallow' : + ''; + + $oot_preclean //= 'no'; + + our @add; + + if (!$cargo_lock_update) { + push @add, qw(--locked) unless subcmd_p('!locked'); + } + if ($linkfarm_depth eq '') { + if (defined($oot_dir) && !subcmd_p('!manifest-path')) { + my $cargotoml = "${src_absdir}/Cargo.toml"; + push @args_preface, "--manifest-path=$cargotoml" if $pass_options; + push @add, qw(--target-dir=target) unless subcmd_p('!target-dir'); + } + } + + if (defined($target) && !subcmd_p('!target')) { if ($target =~ m{^[A-Z]}) { $target = (cfgs 'arch', $target) // $archmap{$target} // die "$self: --target=$target alias specified; not in cfg or map\n"; } - push @ARGV, "--target=$target"; + push @add, "--target=$target"; + } + + push @add, "--offline" unless $online || subcmd_p('!offline'); + + if (subcmd_p('creates') && $linkfarm_depth !~ m/^copy-edit-all/) { + forceable_warning + "this subcommand expects to create new source files; you probably want to specify --edits-sources twice aka -EE (which is not the default even now, for safety reasons)"; + } elsif (subcmd_p('edits') && $linkfarm_depth !~ m/^copy-edit/) { + forceable_warning + "this subcommand expects to edit the source code; you probably want to specify --edits-sources aka -E (which is not the default even now, for safety reasons)"; } + + push @args_preface, @add if $pass_options; + die if grep { m/ / } @add; + $ENV{NAILINGCARGO_CARGO_OPTIONS} = "@add"; + + unshift @ARGV, @args_preface; } -our $oot_absdir; our $build_absdir; # .../Build/ sub oot_massage_cmdline () { return unless defined $oot_dir; my $use = cfgs qw(oot use); - $oot_absdir = ($oot_dir !~ m{^/} ? "$worksphere/" : ""). $oot_dir; + $use // die "$self: out-of-tree build, but \`oot.use' not configured\n"; $build_absdir = "$oot_absdir/$subdir"; - my ($pre,$post); + my ($pre,$post) = ('',''); my @xargs; - if (!$cargo_lock_update) { + if ($linkfarm_depth eq '') { push @xargs, $build_absdir; ($pre, $post) = ('cd "$1"; shift; ', ''); } else { push @xargs, $oot_absdir, $subdir, $src_absdir; - $pre = <<'END'; - cd "$1"; shift; - mkdir -p -- "$1"; cd "$1"; shift; - cp -- "$1"/Cargo.toml -END - $pre .= <<'ENDLK' if stat_exists 'Cargo.lock', 'working cargo lockfile'; - "$1"/Cargo.lock + $pre = <<'END_BOTH'; + bld="$1"; shift; sd="$1"; shift; src="$1"; shift; + cd "$bld"; mkdir -p -- "$sd"; cd "$sd"; +END_BOTH + if ($oot_preclean ne 'no') { + $pre.= "find . -maxdepth 1 ! -path ."; + $pre.= " ! -path ./target" if $oot_preclean ne 'full'; + $pre.= " -print0 | xargs -0r rm -r --;" + } + if ($linkfarm_depth eq 'shallow') { + $pre.= <<'END_SHALLOW'; + clean () { find -lname "$src/*" -print0 | xargs -0r rm --; }; clean; + find "$src" -maxdepth 1 \! -name Cargo.lock -print0 | + xargs -0r sh -ec 'for f in "$@"; do + rm -rf "${f##*/}"; + ln -sf -- "$f" .; + done'; +END_SHALLOW + } elsif ($linkfarm_depth =~ /full|git/) { + $pre .= <<'END_EITHER_DEEP_DIRS'; + clean () { find -follow -lname "$src/*" -print0 | xargs -0r rm --; }; + (set -e; cd "$src"; find . \! -name Cargo.lock \! \( -name .git -prune \) \! -path . \! -name .git -type d -print0) | + xargs -0r sh -ec 'for f in "$@"; do + rm -f "$f" 2>/dev/null ||:; + mkdir -p "$f"; + done' x; +END_EITHER_DEEP_DIRS + if ($linkfarm_depth eq 'git') { + $pre .= <<'END_FILES_GIT' + (set -e; cd "$src"; git ls-files --exclude-standard -co -z) | +END_FILES_GIT + } elsif ($linkfarm_depth eq 'full') { + $pre .= <<'END_FILES_FULL' + (set -e; cd "$src"; find . \! -name Cargo.lock \! \( -name .git -prune \) \! -type d -print0) | +END_FILES_FULL + } + $pre .= <<'END_DEEP'; + perl -0 -ne ' + BEGIN { $src=shift @ARGV; } + next if (readlink "$_"//"") eq "$src/$_"; + unlink "$_"; + symlink "$src/$_", "$_" or die "$_ $!"; + ' "$src"; +END_DEEP + } elsif ($linkfarm_depth =~ m/^copy-edit/) { + $pre .= <<'END_COPY_EDIT'; + find -lname "$src/*" -print0 | xargs -0r rm --; + (set -e; cd "$src"; git ls-files -c -z | + cpio --quiet -p0m --no-preserve-owner -u --make-directories "$bld/$sd"); + clean () { + (set -e; cd "$src"; git ls-files -c -z) | xargs -0r rm -f --; + }; +END_COPY_EDIT + if ($linkfarm_depth eq 'copy-edit-all') { + $post .= <<'END_COPY_EDIT_GENFILES_ALL'; + find -xdev \( \( -name .git -o -path ./target -o -path ./nailing-cargo-update.tar \) -prune \) -o + \( -type l -o -type f \) -print0 | +END_COPY_EDIT_GENFILES_ALL + } else { + $post .= <<'END_COPY_EDIT_GENFILES_GIT'; + (set -e; cd "$src"; git ls-files -c -z) | +END_COPY_EDIT_GENFILES_GIT + } + $post .= <<'END_COPY_EDIT_BUNDLE'; + cpio -Hustar -o0 --quiet >"nailing-cargo-update.tar"; +END_COPY_EDIT_BUNDLE + } else { + die "$linkfarm_depth ?"; + } + $pre .= <<'ENDLK' if $do_cargo_lock; + if test -e Cargo.lock; then + rm -f Cargo.lock; + cp -- "$src"/Cargo.lock .; + fi; ENDLK - $pre .= <<'ENDCP'; - .; -ENDCP - $pre .= <<'ENDPRE'; - shift; - mkdir -p src; >src/lib.rs; >build.rs -ENDPRE - $post = <<'ENDPOST'; - rm -r src Cargo.toml build.rs; -ENDPOST + $post .= <<'ENDCLEAN' if $oot_clean && !$just_linkfarm; + clean; +ENDCLEAN } my $addpath = (cfg_uc qw(oot path_add)) // $use eq 'really' ? Types::Serialiser::true : Types::Serialiser::false; @@ -626,8 +813,9 @@ END } sub setenvs () { - $ENV{NAILINGCARGO_WORKSPHERE} = $worksphere; + $ENV{CARGO_MANIFEST_DIR} = $src_absdir unless $linkfarm_depth; $ENV{NAILINGCARGO_MANIFEST_DIR} = $src_absdir; + $ENV{NAILINGCARGO_WORKSPHERE} = $worksphere; $ENV{NAILINGCARGO_BUILDSPHERE} = $oot_absdir; delete $ENV{NAILINGCARGO_BUILDSPHERE} unless $oot_absdir; $ENV{NAILINGCARGO_BUILD_DIR} = $build_absdir // $src_absdir; @@ -645,6 +833,16 @@ END { } } +sub consider_directories () { + return unless defined $oot_dir; + my $bsubdir = "../$oot_dir/$subdir"; + return if stat $bsubdir; + die "$0: build directory $bsubdir inaccessible\n" + unless $!==ENOENT; + return if $cargo_lock_update; # will make it + die "$0: build directory $bsubdir does not exist, and not in Cargo.lock update mode!\n"; +} + our $cleanup_cargo_lock; sub makebackups () { foreach my $mf (keys %manifests) { @@ -653,6 +851,7 @@ sub makebackups () { } if (defined($alt_cargo_lock)) { + die 'internal error' unless $do_cargo_lock; if (@alt_cargo_lock_stat) { print STDERR "$self: using alt_cargo_lock `$alt_cargo_lock'..." if $verbose>=3; @@ -686,7 +885,18 @@ sub nailed ($) { } sub install () { + my @our_unfound_stab = stat_exists('Cargo.toml', 'local Cargo.toml') + ? (stat _) : (); foreach my $mf (keys %manifests) { + if (@our_unfound_stab) { + if (stat_exists $mf, "manifest in to-be-nailed directory") { + my @mf_stab = stat _ ; + if ("@mf_stab[0..1]" eq "@our_unfound_stab[0..1]") { + @our_unfound_stab = (); + } + } + } + my $nailing = "$mf.nailing~"; my $nailed = nailed($mf); my ($use, $rm); @@ -709,6 +919,11 @@ sub install () { unlink_or_enoent $rm or die "$self: remove old $rm: $!\n"; print STDERR "$self: nailed $mf\n" if $verbose>=3; } + + if (@our_unfound_stab && $do_nail) { + print STDERR + "$self: *WARNING* cwd is not in Cargo.nail thbough it has Cargo.toml!\n"; + } } sub invoke () { @@ -727,8 +942,26 @@ sub invoke () { } } -sub cargo_lock_update_after () { - if ($cargo_lock_update) { +sub files_return_after_update () { + if ($linkfarm_depth =~ m/^copy-edit/) { + my $tar_source_opts; + my $tar_stdin; + if ($linkfarm_depth eq 'copy-edit-all') { + $tar_source_opts = '--null --files-from=-'; + $tar_stdin = <<'END_GIT_FILES'; + git ls-files -c -z | \ +END_GIT_FILES + } else { + $tar_source_opts = '--anchored --exclude=.git --exclude="*/.git" --exclude=target --exclude=nailing-cargo-update.tar'; + $tar_stdin = ''; + } + system qw(sh -ec), $tar_stdin . <<'END', 'x', "$build_absdir"; + tar -x --keep-newer-files --no-same-permissions --no-same-owner \ + --no-acls --no-selinux --no-xattrs --warning=no-ignore-newer \ + -Hustar $tar_source_opts --force-local \ + -f "$1/nailing-cargo-update.tar" +END + } elsif ($do_cargo_lock && $cargo_lock_update && !$just_linkfarm) { # avoids importing File::Copy and the error handling is about as good $!=0; $?=0; my $r= system qw(cp --), "$build_absdir/Cargo.lock", "Cargo.lock"; @@ -747,7 +980,7 @@ sub uninstall1 ($$) { sub unaltcargolock ($) { my ($enoentok) = @_; return unless $cleanup_cargo_lock; - die 'internal error!' unless defined $alt_cargo_lock; + die 'internal error!' unless $do_cargo_lock && defined $alt_cargo_lock; # we ignore $enoentok because we don't know if one was supposed to # have been created. @@ -769,53 +1002,181 @@ sub uninstall () { unaltcargolock(0); } -while (@ARGV && $ARGV[0] =~ m/^-/) { - $_ = shift @ARGV; - last if m{^--$}; - if (m{^-[^-]}) { - while (m{^-.}) { - if (s{^-v}{-}) { - $verbose++; - } elsif (s{^-q}{-}) { - $verbose=0; - } elsif (s{^-n}{-}) { - $noact++; - } elsif (s{^-D}{-}) { - $dump++; - } elsif (s{^-T(.+)}{-}s) { - $target = $1; - } elsif (s{^-([uU])}{-}) { - $cargo_lock_update= $1=~m/[a-z]/; - } elsif (s{^-([mM])}{-}) { - $cargo_manifest_args= $1=~m/[a-z]/; - } elsif (s{^-([tT])}{-}) { - $cargo_target_arg= $1=~m/[a-z]/; - } else { - die "$self: unknown short option(s) $_\n"; +sub parse_args () { + my $is_cargo; + + # Loop exit condition: + # $is_cargo is set + # @ARGV contains + # $is_cargo==1 [--] ... + # $is_cargo==0 ... + + OPTS: for (;;) { + if (!@ARGV) { + die "$self: need cargo subcommand\n" + unless $noact || $just_linkfarm;; + push @ARGV, "CARGO-SUBCOMMAND"; # dummy, user may see it + } + + $_ = shift @ARGV; + my $orgopt = $_; + + my $not_a_nailing_opt = sub { # usage 1 + unshift @ARGV, $orgopt; + unshift @ARGV, 'cargo'; + $is_cargo = 1; + no warnings qw(exiting); + last OPTS; + }; + $not_a_nailing_opt->() unless m{^-}; + $not_a_nailing_opt->() if $_ eq '--'; + + my $edits_sources = sub { + $linkfarm_depth = + ($linkfarm_depth//'') eq 'copy-edit' ? 'copy-edit-all' : 'copy-edit'; + }; + + if ($_ eq '---') { # usage 2 or 3 + if (!@ARGV) { + die "$self: --- must be followed by build command\n" unless $noact; + push @ARGV, 'BUILD-COMMAND'; + } + if ($ARGV[0] eq '--') { # usage 3 + shift; + $is_cargo = 0; + } elsif (grep { $_ eq '--' } @ARGV) { # usage 2 + $is_cargo = 1; + } elsif ($ARGV[0] =~ m{[^/]*cargo[^/]*$}) { # usage 2 + $is_cargo = 1; + } else { # usage 3 + $is_cargo = 0; + } + last; + } + if (m{^-[^-]}) { + while (m{^-.}) { + if (s{^-h}{-}) { + print_usage(); + } elsif (s{^-v}{-}) { + $verbose++; + } elsif (s{^-q}{-}) { + $verbose=0; + } elsif (s{^-n}{-}) { + $noact++; + } elsif (s{^-f}{-}) { + $force++; + } elsif (s{^-s(.+)}{-}s) { + $cargo_subcmd = $1; + } elsif (s{^-([uU])}{-}) { + $cargo_lock_update = $1=~m/[a-z]/; + } elsif (s{^-([cC])}{-}) { + $pass_options = $1=~m/[a-z]/; + } elsif (s{^-D}{-}) { + $dump++; + } elsif (s{^-E}{-}) { + $edits_sources->(); + } elsif (s{^-T(.+)}{-}s) { + $target = $1; + } elsif (s{^-([oO])}{-}) { + $online = $1=~m/[a-z]/; + } else { + die "$self: unknown short option(s) $_\n" unless $_ eq $orgopt; + $not_a_nailing_opt->(); + } + } + } elsif (s{^--help$}{}) { + print_usage(); + } elsif (s{^--(?:doc|man|manual)?$}{}) { + show_manual(); + } elsif (s{^--target=}{}) { + $target = $_; + } elsif (m{^--(on|off)line$}) { + $online = $1 eq 'on'; + } elsif (m{^--just-linkfarm(?:=(shallow|git|full))?$}) { + $just_linkfarm = 1; + $linkfarm_depth = $1 if $1; + $cargo_lock_update= 1; # will set $linkfarm_detph to 1 by default + } elsif (m{^--linkfarm(?:=(no|shallow|git|full))?$}) { + $linkfarm_depth = $1 || 'git'; + } elsif (m{^--edits?-sources?$}) { + $edits_sources->(); + } elsif (m{^--force$}) { + $force++; + } elsif (m{^--just-run$}) { + $do_nail = $do_cargo_lock = $do_lock = 0; + } elsif (m{^--(clean|keep)-linkfarm$}) { + $oot_clean = $1 eq 'clean'; + } elsif (m{^--(no-)?preclean-build$}) { + $oot_preclean = $1 ? 'no' : 'src'; + } elsif (m{^--preclean-build=(no|src|full)$}) { + $oot_preclean = $1; + } elsif (m{^--(no-)?nail$}) { + $do_nail = !$1; + } elsif (m{^--(no-)?cargo-lock-manip$}) { + $do_cargo_lock = !$1; + } elsif (m{^--(no-)?concurrency-lock$}) { + $do_lock = !$1; + } elsif (m{^--leave-nailed$}) { + $leave_nailed = 1; + } elsif (s{^--subcommand-props=}{}) { + my @props = split /\,/, $_; + our %subcmd_prop_ok; + if (!%subcmd_prop_ok) { + foreach my $v (\@subcmd_xprops, values %subcmd_props) { + $subcmd_prop_ok{$_}=1 foreach @$v; + }; } + $subcmd_prop_ok{$_} + or die "$self: unknown subcommand property \`$_'\n" + foreach @props; + $cargo_subcmd = \@props; + } elsif (m{^--(no-)?cargo-lock-update}) { + $cargo_lock_update= !!$1; + } else { + $not_a_nailing_opt->(); } - } elsif (s{^--target=}{}) { - $target = $_; - } elsif (m{^--(no-)?cargo-lock-update}) { - $cargo_lock_update= !!$1; - } elsif (m{^--(no-)?cargo-manifest-args}) { - $cargo_manifest_args= !!$1; - } elsif (m{^--(no-)?cargo-target-arg}) { - $cargo_target_arg= !!$1; + } + + $is_cargo // die; + @ARGV || die; + + if ($is_cargo) { + @args_preface = shift @ARGV; + while (defined($_ = shift @ARGV)) { + if (!m{^-|^\+}) { unshift @ARGV, $_; last; } + if ($_ eq '--') { last; } + push @args_preface, $_; + } + @ARGV || die "$self: need cargo subcommand\n"; + $cargo_subcmd //= $ARGV[0]; + $pass_options //= 1; } else { - die "$self: unknown long option $_\n"; + $cargo_subcmd //= ''; + $pass_options //= 0; } -} + push @args_preface, shift @ARGV; -die "$self: need command to run\n" unless @ARGV || $noact; + if (!ref($cargo_subcmd)) { + print STDERR " cargo_subcmd lookup $cargo_subcmd\n" if $dump; + $cargo_subcmd = $subcmd_props{$cargo_subcmd} // [ ]; + } -takelock(); + print STDERR " cargo_subcmd props @$cargo_subcmd\n" if $dump; + my %cargo_subcmd; + $cargo_subcmd{$_} = 1 foreach @$cargo_subcmd; + $cargo_subcmd = \%cargo_subcmd; +} + +parse_args(); +loadconfigs(); readnail(); +takelock(); consider_alt_cargo_lock(); consider_oot(); readorigs(); calculate(); addargs(); +consider_directories(); our @display_cmd = @ARGV; oot_massage_cmdline(); setenvs(); @@ -830,29 +1191,34 @@ if ($dump) { subdir => $subdir, oot_dir => $oot_dir, oot_absdir => $oot_absdir, - build_absdir => $build_absdir }); + build_absdir => $build_absdir, + linkfarm_depth => $linkfarm_depth, + oot_preclean => $oot_preclean, + force => $force,, + forced => $forced,}); ' or die $@; } exit 0 if $noact; -$want_uninstall = 1; +$want_uninstall = !$leave_nailed; makebackups(); install(); printf STDERR "$self: nailed (%s manifests, %s packages)%s\n", (scalar keys %manifests), (scalar keys %packagemap), (defined($alt_cargo_lock) and ", using `$alt_cargo_lock'") - if $verbose; + if $verbose && $do_nail; print STDERR "$self: invoking: @display_cmd\n" if $verbose; my $estatus = invoke(); -cargo_lock_update_after(); +files_return_after_update(); -uninstall(); +uninstall() unless $leave_nailed; $want_uninstall = 0; -print STDERR "$self: unnailed. status $estatus.\n" if $verbose; +print STDERR "$self: ".($do_nail ? "unnailed" : "finished") + .". status $estatus.\n" if $verbose; exit $estatus;