#!/bin/bash
# Run this script in an unpacked upstream tarball directory, and it will
# do two things:
#
# First, it will update (i.e. overwrite) the "unused deps" part of
# Files-Excluded in d/copyright.
#
# Second, it will create a patch which adds in the stubs of unwanted
# vendored crates in order to allow the build to run.

# This script requires an up-to-date Rust toolchain with cargo-vendor-filterer
# installed; see https://crates.io/crates/cargo-vendor-filterer/ for more info.

set -e

function cleanup() {
    set +e

    cd "$scriptdir"

    # Restore the lockfiles of the given manifests
    git restore "Cargo.lock"
    for manifest in ${project_manifests[@]}; do
        if [ -f "${manifest::-4}lock" ]; then
            git restore "${manifest::-4}lock"
        fi
    done

    rm -f "$scriptdir/debian/copyright.unused-deps"

    if [ -d "$vendor_dir_backup" ]; then
        rm -rf "$vendor_dir"
        mv "$vendor_dir_backup" "$vendor_dir"
    fi

    if [ -f "$patch_series_backup" ]; then
        mv "$patch_series_backup" "$patch_series"
    fi

    if [ -f "$std_manifest_backup" ]; then
        mv "$std_manifest_backup" "$std_manifest"
    fi

    quilt pop -a >/dev/null 2>&1

    rm -rf "$scriptdir/.pc"
}
trap cleanup EXIT

# List of supported platforms; edit as needed
supported_platforms=(
    "x86_64-unknown-linux-gnu"
    "aarch64-unknown-linux-gnu"
    "i686-unknown-linux-gnu"
    "armv7-unknown-linux-gnueabihf"
    "powerpc64le-unknown-linux-gnu"
    "powerpc-unknown-linux-gnu"
    "s390x-unknown-linux-gnu"
    "riscv64gc-unknown-linux-gnu"
)

# Consult the hard-coded list of workspaces to vendor in
# src/bootstrap/src/core/build_steps/vendor.rs, then comment out any
# that aren't used by this package
project_manifests=(
    "src/tools/cargo/Cargo.toml"         # cargo
    "src/tools/rust-analyzer/Cargo.toml" # rust-analyzer
    # "compiler/rustc_codegen_cranelift/Cargo.toml" # cranelift codegen
    # "compiler/rustc_codegen_gcc/Cargo.toml" # GCC codegen
    "library/Cargo.toml"            # libraries
    "src/bootstrap/Cargo.toml"      # bootstrap
    "src/tools/rustbook/Cargo.toml" # rustbook
    # "src/tools/rustc-perf/Cargo.toml" # rustc performance graph
    # "src/tools/opt-dist/Cargo.toml"         # PGO and BOLT optimizer
    # "src/doc/book/packages/trpl/Cargo.toml" # The Rust Programming Language
)

# Certain dependencies in the standard library manifest
# (library/std/Cargo.toml) are marked as optional, but are actually
# required by the backtrace crate.
#
# They aren't normally picked up by cargo-vendor-filterer, so they must
# be temporarily marked as non-optional while running
# cargo-vendor-filterer. The standard library manifest itself is not
# impacted in any way for the rest of the build process.
forced_std_deps=(
    "miniz_oxide"
    "addr2line"
    "object"
)

# ============================================================
# PART 1: update d/copyright Files-Excluded with unwanted deps
# ============================================================

scriptdir=$(dirname "$(dirname "$(readlink -f "$0")")")

stub_patch="prune/d-0021-add-pruned-dep-stubs.patch"

patch_series="$scriptdir/debian/patches/series"
patch_series_backup="$patch_series.bak"

# Temporarily remove old stub patch from the patch series (if it exists)
cp "$patch_series" "$patch_series_backup"
sed -i "\|$stub_patch|d" "$patch_series"

quilt push -a

vendored_deps=$(mktemp -du)
stubbed_deps=$(mktemp -d)
vendor_filterer_output=$(mktemp)

vendor_filterer_args=()
for platform in "${supported_platforms[@]}"; do
    vendor_filterer_args+=("--platform=$platform")
done
for manifest in "${project_manifests[@]}"; do
    vendor_filterer_args+=("--sync=$manifest")
done

# Temporarily force-include backtrace dependencies
std_manifest="$scriptdir/library/std/Cargo.toml"
std_manifest_backup="$std_manifest.backup"

cp "$std_manifest" "$std_manifest_backup"

function std_dep_make_non_optional() {
    local crate=$1

    if ! grep -q "$crate = {.*optional = true.*" "$std_manifest"; then
        echo "optional dependency $crate not found in $std_manifest" >&2
        exit 1
    fi

    sed -i \
        "s/\($crate = {.*\)optional = true\(.*\)/\1optional = false\2/" \
        "$std_manifest"
}

for forced_std_dep in "${forced_std_deps[@]}"; do
    std_dep_make_non_optional "$forced_std_dep"
done

# Get the vendored and stubbed dependencies needed for all supported
# platforms
if cargo +nightly vendor-filterer "$vendored_deps" \
    --versioned-dirs \
    "${vendor_filterer_args[@]}" \
    2>&1 | tee "$vendor_filterer_output"; then
    stubbed_list=$(grep -oP \
        '(?<=Replacing unreferenced package with stub: ).*' \
        "$vendor_filterer_output")
else
    echo "cargo-vendor-filterer failed" >&2
    exit 1
fi

# Restore the actual std manifest
mv "$std_manifest_backup" "$std_manifest"

# Move all the stubbed outputs to their own dir
for stub in $stubbed_list; do
    mv "$vendored_deps/$stub" "$stubbed_deps"
done

# Since we'll be entirely replacing the stubbed deps, we can exclude
# all stubbed _or_ pruned dependencies, then format them as file paths
# for d/copyright
exclusion_list=$(comm -23 \
    <(ls -1A "$scriptdir/vendor" | sort) \
    <(ls -1A "$vendored_deps" | sort) |
    while read line; do echo " vendor/$line"; done)

# Overwrite the "unused deps" part of Files-Excluded in d/copyright
header='# DO NOT EDIT below, AUTOGENERATED'
footer='# DO NOT EDIT above, AUTOGENERATED'
{
    echo "$header"
    echo "$exclusion_list"
    echo "$footer"
} >$scriptdir/debian/copyright.unused-deps
cd $scriptdir/debian
sed -i -e "/^$header/,/^$footer/d" -e '/^# unused dependencies/rcopyright.unused-deps' copyright
cd - >/dev/null

# ===========================================================
# PART 2: Create a patch to add the unwanted dependency stubs
# ===========================================================

# We know that later on, the maintainer will have repacked the orig
# tarball, excluding the crates that will be stubbed. Therefore, we must
# simulate those conditions to generate the diff for the patch.
vendor_dir="$scriptdir/vendor"
vendor_dir_backup="$scriptdir/vendor-backup"

# Replace vendor/ with an empty dir
rm -rf "$vendor_dir_backup" || true
mv "$vendor_dir" "$vendor_dir_backup"
mkdir "$vendor_dir"

cd "$scriptdir"

quilt new "$stub_patch"

# Add the empty files to the patch
for crate_stub in $(ls -1A "$stubbed_deps"); do
    quilt add "vendor/$crate_stub/Cargo.toml"
    quilt add "vendor/$crate_stub/.cargo-checksum.json"
    quilt add "vendor/$crate_stub/src/lib.rs"
done
quilt refresh

# Add a newline to the empty lib.rs files, otherwise quilt won't include
# them in the patch
for crate_stub in $(ls -1A "$stubbed_deps"); do
    printf '\n' >>"$stubbed_deps/$crate_stub/src/lib.rs"
done

# Add the stubs to the dummy vendor dir
cp -r "$stubbed_deps"/* "$vendor_dir"
quilt refresh

# Add the patch header
if [ -z "$DEBFULLNAME" ] || [ -z "$DEBEMAIL" ]; then
    echo "DEBFULLNAME and/or DEBEMAIL have not been set properly; the patch header cannot be generated" >&2
    quilt pop -f >/dev/null
    exit 1
fi
echo "Description: Generate unwanted dependency stubs
This package's orig tarball has been pruned of all vendored
dependencies that aren't needed for Linux platforms. However, with
these dependencies removed, the dependencies won't align with the
lockfile.
.
To solve this problem, this patch generates crate stubs, which consist
of a checksum, an empty lib.rs file, and a Cargo.toml containing the
minimal amount of information needed to run the build.
.
These stubs are generated automatically by running d/prune-unused-deps,
which uses cargo-vendor-filterer
(https://crates.io/crates/cargo-vendor-filterer/) to generate them.
Author: $DEBFULLNAME <$DEBEMAIL>
---" |
    quilt header -r --

quilt pop

# The original vendor dir must be restored so the other patches remove cleanly
rm -rf "$vendor_dir"
mv "$vendor_dir_backup" "$vendor_dir"
quilt pop -a

cd - >/dev/null

rm -f "$patch_series_backup"

echo "prune-unused-deps finished successfully. The Files-Excluded changes in d/copyright and the new d/p/$stub_patch patch have been generated."
