#!/bin/sh
#
# step1-create-recover-list.sh
#
# /var/db/ports 復元候補となる ports origin の一覧を生成する。
#
# 判定方針:
#   - pkg database に built_by Annotation があるパッケージは、
#     Poudriere等で作成されたバイナリパッケージ由来とみなし除外する。
#   - built_by Annotation がないパッケージを、portsから直接
#     インストールされた可能性があるものとして復元候補にする。
#
# このスクリプトは /var/db/ports を変更しない。
#
# 出力:
#   recover.list
#       復元候補となる、現在のports treeに存在するorigin
#
#   recover-packages.tsv
#       復元候補となったパッケージの詳細
#
#   binary-packages.tsv
#       built_byが存在するため除外されたパッケージ
#
#   missing-origins.tsv
#       現在のports treeにoriginが存在しない復元候補
#
#   no-origin-packages.tsv
#       originが空または不正なパッケージ
#
# 使用例:
#   mkdir /root/ports-options-recovery
#   cd /root/ports-options-recovery
#   sh ./step1-create-recover-list.sh
#

set -eu

PORTSDIR=${PORTSDIR:-/usr/ports}
OUTDIR=${OUTDIR:-.}

RECOVER_LIST="${OUTDIR}/recover.list"
RECOVER_PACKAGES="${OUTDIR}/recover-packages.tsv"
BINARY_PACKAGES="${OUTDIR}/binary-packages.tsv"
MISSING_ORIGINS="${OUTDIR}/missing-origins.tsv"
NO_ORIGIN_PACKAGES="${OUTDIR}/no-origin-packages.tsv"

TMPDIR_BASE=${TMPDIR:-/tmp}
WORKDIR=$(mktemp -d "${TMPDIR_BASE}/ports-options-step1.XXXXXX")

cleanup()
{
    rm -rf "${WORKDIR}"
}
trap cleanup EXIT HUP INT TERM

ALL_PACKAGES="${WORKDIR}/all-packages.tsv"
BUILT_BY_PACKAGES="${WORKDIR}/built-by-packages.tsv"
BUILT_BY_NAMES="${WORKDIR}/built-by-names"
CANDIDATE_PACKAGES="${WORKDIR}/candidate-packages.tsv"
VALID_ORIGINS="${WORKDIR}/valid-origins"

if ! command -v pkg >/dev/null 2>&1; then
    echo "Error: pkg command was not found." >&2
    exit 1
fi

if [ ! -d "${PORTSDIR}" ]; then
    echo "Error: ports tree was not found: ${PORTSDIR}" >&2
    echo "Set PORTSDIR if the ports tree is located elsewhere." >&2
    exit 1
fi

mkdir -p "${OUTDIR}"

echo "Reading installed package information..."

# package name, version, origin, automatic flag
#
# %a:
#   0 = manually installed
#   1 = automatically installed as a dependency
#
# automatic flagは今回の選別には使用しないが、
# 後から対象を人間が確認する際の参考情報として保存する。
pkg query '%n	%v	%o	%a' |
    sort -t '	' -k1,1 > "${ALL_PACKAGES}"

# %At はAnnotationのタグ名、%Avはその値。
#
# 一つのパッケージに複数のAnnotationがあるため、
# built_byタグを持つ行だけを抽出する。
pkg query '%n	%v	%o	%a	%At	%Av' |
    awk -F '	' '$5 == "built_by"' |
    sort -t '	' -k1,1 > "${BUILT_BY_PACKAGES}"

cut -f1 "${BUILT_BY_PACKAGES}" |
    sort -u > "${BUILT_BY_NAMES}"

# built_byを持たないパッケージだけを候補にする。
#
# package nameはpkg database内で一意なので、
# 第一フィールドをキーとして比較する。
awk -F '	' '
    NR == FNR {
        excluded[$1] = 1
        next
    }
    !($1 in excluded) {
        print
    }
' "${BUILT_BY_NAMES}" "${ALL_PACKAGES}" > "${CANDIDATE_PACKAGES}"

# 出力ファイルを初期化する。
: > "${RECOVER_PACKAGES}"
: > "${BINARY_PACKAGES}"
: > "${MISSING_ORIGINS}"
: > "${NO_ORIGIN_PACKAGES}"
: > "${VALID_ORIGINS}"

printf 'package\tversion\torigin\tautomatic\tbuilt_by\n' \
    > "${BINARY_PACKAGES}"

awk -F '	' '
    BEGIN {
        OFS = "\t"
    }
    {
        print $1, $2, $3, $4, $6
    }
' "${BUILT_BY_PACKAGES}" >> "${BINARY_PACKAGES}"

printf 'package\tversion\torigin\tautomatic\n' \
    > "${RECOVER_PACKAGES}"

printf 'package\tversion\torigin\tautomatic\treason\n' \
    > "${MISSING_ORIGINS}"

printf 'package\tversion\torigin\tautomatic\treason\n' \
    > "${NO_ORIGIN_PACKAGES}"

while IFS="$(printf '\t')" read -r pkgname version origin automatic
do
    case "${origin}" in
        ""|"-")
            printf '%s\t%s\t%s\t%s\t%s\n' \
                "${pkgname}" "${version}" "${origin}" "${automatic}" \
                "package has no usable origin" \
                >> "${NO_ORIGIN_PACKAGES}"
            continue
            ;;
    esac

    # 絶対パスや ".." を含む異常なoriginを除外する。
    case "${origin}" in
        /*|../*|*/../*|*/..)
            printf '%s\t%s\t%s\t%s\t%s\n' \
                "${pkgname}" "${version}" "${origin}" "${automatic}" \
                "invalid origin" \
                >> "${NO_ORIGIN_PACKAGES}"
            continue
            ;;
    esac

    if [ -d "${PORTSDIR}/${origin}" ] &&
       [ -f "${PORTSDIR}/${origin}/Makefile" ]; then
        printf '%s\t%s\t%s\t%s\n' \
            "${pkgname}" "${version}" "${origin}" "${automatic}" \
            >> "${RECOVER_PACKAGES}"

        printf '%s\n' "${origin}" >> "${VALID_ORIGINS}"
    else
        printf '%s\t%s\t%s\t%s\t%s\n' \
            "${pkgname}" "${version}" "${origin}" "${automatic}" \
            "origin not found in current ports tree" \
            >> "${MISSING_ORIGINS}"
    fi
done < "${CANDIDATE_PACKAGES}"

sort -u "${VALID_ORIGINS}" > "${RECOVER_LIST}"

all_count=$(wc -l < "${ALL_PACKAGES}" | tr -d ' ')
binary_count=$(awk 'NR > 1 { count++ } END { print count + 0 }' \
    "${BINARY_PACKAGES}")
candidate_pkg_count=$(awk 'NR > 1 { count++ } END { print count + 0 }' \
    "${RECOVER_PACKAGES}")
origin_count=$(wc -l < "${RECOVER_LIST}" | tr -d ' ')
missing_count=$(awk 'NR > 1 { count++ } END { print count + 0 }' \
    "${MISSING_ORIGINS}")
no_origin_count=$(awk 'NR > 1 { count++ } END { print count + 0 }' \
    "${NO_ORIGIN_PACKAGES}")

cat <<EOF

Step 1 completed.

Installed packages:                  ${all_count}
Excluded packages with built_by:    ${binary_count}
Candidate packages with valid port: ${candidate_pkg_count}
Unique candidate origins:           ${origin_count}
Origins absent from current tree:   ${missing_count}
Packages without usable origin:     ${no_origin_count}

Generated files:

  ${RECOVER_LIST}
      Origins proposed for recovery.

  ${RECOVER_PACKAGES}
      Packages selected as recovery candidates.

  ${BINARY_PACKAGES}
      Packages excluded because built_by is present.

  ${MISSING_ORIGINS}
      Candidate packages whose origins are absent from ${PORTSDIR}.

  ${NO_ORIGIN_PACKAGES}
      Packages without a usable origin.

Review ${RECOVER_LIST} before proceeding to Step 2.
EOF

