#!/bin/sh
# Build a matched CCL kernel and saved image. Each complete pair lives in a
# private release directory; one symlink rename activates its kernel.
set -eu
umask 077
cd "$(dirname "$0")/.."
if ! scripts/build-lock-held; then
    exec scripts/with-build-lock scripts/build "$@"
fi
if [ "${CCLSH_BUILD_STATE_TRANSACTION:-0}" != 1 ]; then
    CCLSH_BUILD_STATE_TRANSACTION=1
    export CCLSH_BUILD_STATE_TRANSACTION
    exec scripts/with-build-state-rollback scripts/build "$@"
fi
validation_home=
build_staging=
build_transaction=
temporary_link=

cleanup_staged_build()
{
    rm -f cclsh.new cclsh.image.new
    rm -rf -- .cclsh-quicklisp-build.*
    if [ -n "$validation_home" ]; then
        rm -rf -- "$validation_home"
    fi
    if [ -n "$build_staging" ]; then
        rm -rf -- "$build_staging"
    fi
    if [ -n "$build_transaction" ]; then
        rm -rf -- "$build_transaction"
    fi
    if [ -n "$temporary_link" ]; then
        rm -f -- "$temporary_link"
    fi
}
trap cleanup_staged_build 0
trap 'exit 129' 1
trap 'exit 130' 2
trap 'exit 143' 15
cleanup_staged_build
rm -f cclsh.attestation
ccl_command=${CCLSH_CCL:-ccl}
if ! command -v "$ccl_command" >/dev/null 2>&1; then
    echo "cclsh build: CCL executable not found: $ccl_command" >&2
    exit 1
fi
validation_timeout=${CCLSH_BUILD_VALIDATION_TIMEOUT:-30}
case "$validation_timeout" in
    ''|*[!0-9]*)
        echo "cclsh build: validation timeout must be positive seconds" >&2
        exit 2
        ;;
esac
case "$validation_timeout" in
    *[1-9]*) ;;
    *)
        echo "cclsh build: validation timeout must be positive seconds" >&2
        exit 2
        ;;
esac
if [ ! -x /usr/bin/timeout ]; then
    echo "cclsh build: /usr/bin/timeout is required" >&2
    exit 1
fi
set -- -n -b
if [ -n "${CCLSH_CCL_IMAGE:-}" ]; then
    set -- -I "$CCLSH_CCL_IMAGE" "$@"
fi
"$ccl_command" "$@" \
    -e '(setf ccl:*default-file-character-encoding* :utf-8)' \
    -e \
    '(handler-case
         (load "scripts/build.lisp" :external-format :utf-8)
       (serious-condition (condition)
         (format *error-output* "cclsh build loader failed: ~a~%" condition)
         (ccl:quit 1)))'
if [ ! -s cclsh.new ]; then
    echo "cclsh build: the CCL kernel was not copied" >&2
    exit 1
fi
if [ ! -s cclsh.image.new ]; then
    echo "cclsh build: cclsh.image.new was not created" >&2
    exit 1
fi
chmod 755 cclsh.new
echo "Validating saved cclsh image..."
validation_home=$(
    mktemp -d "$PWD/.cclsh-build-validation-home.XXXXXX"
)
locked_dependency_commit()
{
    dependency_name=$1
    dependency_prefix=$dependency_name=
    dependency_commit=
    dependency_count=0
    while IFS= read -r dependency_line || [ -n "$dependency_line" ]; do
        case "$dependency_line" in
            "$dependency_prefix"*)
                dependency_commit=${dependency_line#"$dependency_prefix"}
                dependency_count=$((dependency_count + 1))
                ;;
        esac
    done <dependencies.lock
    if [ "$dependency_count" -ne 1 ] ||
       [ "${#dependency_commit}" -ne 40 ]
    then
        return 1
    fi
    case "$dependency_commit" in
        *[!0123456789abcdef]*) return 1 ;;
    esac
    printf '%s\n' "$dependency_commit"
}
if ! expected_clinedi_commit=$(locked_dependency_commit clinedi) ||
   ! expected_structlisp_commit=$(locked_dependency_commit structlisp) ||
   ! expected_cl_colorist_commit=$(locked_dependency_commit cl-colorist) ||
   ! expected_colorlisp_commit=$(locked_dependency_commit colorlisp) ||
   ! expected_colordiff_commit=$(locked_dependency_commit colordiff)
then
    echo "cclsh build: dependencies.lock has invalid dependency revisions" >&2
    exit 1
fi
build_root=$PWD
if ! (
     cd "$validation_home"
     HOME="$validation_home" \
     XDG_CACHE_HOME="$validation_home/cache" \
     XDG_CONFIG_HOME="$validation_home/config" \
     XDG_DATA_HOME="$validation_home/data" \
     CCLSH_SAFE=1 \
     CCLSH_QUICKLISP_SETUP="$validation_home/setup.lisp" \
     CCLSH_EXPECTED_CLINEDI_COMMIT="$expected_clinedi_commit" \
     CCLSH_EXPECTED_STRUCTLISP_COMMIT="$expected_structlisp_commit" \
     CCLSH_EXPECTED_CL_COLORIST_COMMIT="$expected_cl_colorist_commit" \
     CCLSH_EXPECTED_COLORLISP_COMMIT="$expected_colorlisp_commit" \
     CCLSH_EXPECTED_COLORDIFF_COMMIT="$expected_colordiff_commit" \
     LANG=C \
     LC_ALL=C \
     /usr/bin/timeout --signal=TERM --kill-after=1 \
     "$validation_timeout" \
     "$build_root/cclsh.new" -I "$build_root/cclsh.image.new" -c \
     '(let* ((quicklisp-package (find-package "QL"))
             (quickload
               (and quicklisp-package
                    (find-symbol "QUICKLOAD" quicklisp-package)))
             (local-directories
               (and quicklisp-package
                    (find-symbol "*LOCAL-PROJECT-DIRECTORIES*"
                                 quicklisp-package)))
             (clinedi-package (find-package "CLINEDI"))
             (edit-line
               (and clinedi-package
                    (find-symbol "EDIT-LINE" clinedi-package)))
             (text-cell-width
               (and clinedi-package
                    (find-symbol "TEXT-CELL-WIDTH" clinedi-package)))
             (structlisp-package (find-package "STRUCTLISP"))
             (make-deque
               (and structlisp-package
                    (find-symbol "MAKE-DEQUE" structlisp-package)))
             (cl-colorist-package (find-package "CL-COLORIST"))
             (strip-ansi
               (and cl-colorist-package
                    (find-symbol "STRIP-ANSI" cl-colorist-package)))
             (colorlisp-package (find-package "COLORLISP"))
             (language-detect
               (and colorlisp-package
                    (find-symbol "LANGUAGE-DETECT" colorlisp-package)))
             (colordiff-package (find-package "COLORDIFF"))
             (render-diff
               (and colordiff-package
                    (find-symbol "RENDER-DIFF" colordiff-package)))
             (expected-clinedi
               (cclsh:getenv "CCLSH_EXPECTED_CLINEDI_COMMIT"))
             (expected-structlisp
               (cclsh:getenv "CCLSH_EXPECTED_STRUCTLISP_COMMIT"))
             (expected-cl-colorist
               (cclsh:getenv "CCLSH_EXPECTED_CL_COLORIST_COMMIT"))
             (expected-colorlisp
               (cclsh:getenv "CCLSH_EXPECTED_COLORLISP_COMMIT"))
             (expected-colordiff
               (cclsh:getenv "CCLSH_EXPECTED_COLORDIFF_COMMIT")))
        (when local-directories
          (setf (symbol-value local-directories)
                (list #P"/definitely/missing/editor-local-projects/")))
         (let ((clear-system (find-symbol "CLEAR-SYSTEM" "ASDF")))
           (when (and clear-system (fboundp clear-system))
              (dolist (system (quote ("clinedi" "structlisp" "cl-colorist")))
                (funcall clear-system system))))
        (unless (and quickload (fboundp quickload))
          (error "saved image does not provide QL:QUICKLOAD"))
        (unless (and edit-line (fboundp edit-line)
                     text-cell-width (fboundp text-cell-width)
                     (= 4 (funcall text-cell-width "你好")))
          (error "saved image does not contain functional Clinedi code"))
         (unless (and make-deque (fboundp make-deque)
                      (funcall make-deque :maximum-count 1))
           (error "saved image does not contain functional StructLisp code"))
        (unless (and strip-ansi
                     (fboundp strip-ansi)
                     (string=
                      "red"
                      (funcall
                       strip-ansi
                       (format nil "~c[31mred~c[0m"
                               (code-char 27)
                               (code-char 27)))))
          (error "saved image does not contain functional cl-colorist code"))
         (unless (and language-detect
                      (fboundp language-detect)
                      (funcall language-detect "definition.lisp"))
           (error "saved image does not contain functional ColorLisp code"))
         (unless (and render-diff
                      (fboundp render-diff)
                      (funcall render-diff
                               :removed-content "(foo 1)"
                               :added-content "(foo 2)"
                               :source-path "definition.lisp"))
           (error "saved image does not contain functional Colordiff code"))
        (unless (and expected-clinedi
                     (string= expected-clinedi
                              cclsh:*cclsh-build-clinedi-commit*))
          (error "saved image Clinedi commit ~s does not match lock ~s"
                 cclsh:*cclsh-build-clinedi-commit* expected-clinedi))
         (unless (and expected-structlisp
                      (string= expected-structlisp
                               cclsh:*cclsh-build-structlisp-commit*))
           (error "saved image StructLisp commit ~s does not match lock ~s"
                  cclsh:*cclsh-build-structlisp-commit* expected-structlisp))
        (unless (and expected-cl-colorist
                     (string=
                      expected-cl-colorist
                      cclsh:*cclsh-build-cl-colorist-commit*))
          (error
           "saved image cl-colorist commit ~s does not match lock ~s"
           cclsh:*cclsh-build-cl-colorist-commit*
           expected-cl-colorist))
         (unless (and expected-colorlisp
                      (string= expected-colorlisp
                               cclsh:*cclsh-build-colorlisp-commit*))
           (error
            "saved image ColorLisp commit ~s does not match lock ~s"
            cclsh:*cclsh-build-colorlisp-commit*
            expected-colorlisp))
         (unless (and expected-colordiff
                      (string= expected-colordiff
                               cclsh:*cclsh-build-colordiff-commit*))
           (error
            "saved image Colordiff commit ~s does not match lock ~s"
            cclsh:*cclsh-build-colordiff-commit*
            expected-colordiff))
        (values))'
     )
then
    echo "cclsh build: staged saved-image validation failed" >&2
    exit 1
fi
if ! scripts/verify-argument-boundary \
     "$build_root/cclsh.new" "$build_root/cclsh.image.new"
then
    echo "cclsh build: staged kernel failed its argument check" >&2
    exit 1
fi
if ! kernel_hash=$(scripts/file-sha256 cclsh.new); then
    exit 1
fi
if ! image_hash=$(scripts/file-sha256 cclsh.image.new); then
    exit 1
fi
if ! release_hash=$(
    printf '%s\n%s\n' "$kernel_hash" "$image_hash" | sha256sum
); then
    echo "cclsh build: could not hash release identity" >&2
    exit 1
fi
release_hash=${release_hash%% *}
if [ "${#release_hash}" -ne 64 ]; then
    echo "cclsh build: invalid release identity digest" >&2
    exit 1
fi
case "$release_hash" in
    *[!0123456789abcdef]*)
        echo "cclsh build: invalid release identity digest" >&2
        exit 1
        ;;
esac
release_id=$(printf '%.20s' "$release_hash")
releases_directory=.cclsh-build-releases
release_directory="$releases_directory/$release_id"
install -d -m 700 "$releases_directory"
if [ -d "$release_directory" ]; then
    if [ ! -f "$release_directory/cclsh" ] ||
       [ ! -f "$release_directory/cclsh.image" ] ||
       ! cmp -s cclsh.new "$release_directory/cclsh" ||
       ! cmp -s cclsh.image.new "$release_directory/cclsh.image"
    then
        echo "cclsh build: existing release is damaged: $release_directory" >&2
        exit 1
    fi
    chmod 700 "$release_directory"
    chmod 755 "$release_directory/cclsh"
    chmod 600 "$release_directory/cclsh.image"
    rm -f cclsh.new cclsh.image.new
else
    build_staging=$(mktemp -d "$releases_directory/.release.XXXXXX")
    mv cclsh.new "$build_staging/cclsh"
    mv cclsh.image.new "$build_staging/cclsh.image"
    chmod 700 "$build_staging"
    chmod 755 "$build_staging/cclsh"
    chmod 600 "$build_staging/cclsh.image"
    sync -f "$build_staging/cclsh"
    sync -f "$build_staging/cclsh.image"
    sync -f "$build_staging"
    mv -T "$build_staging" "$release_directory"
    build_staging=
    sync -f "$releases_directory"
fi

build_transaction=$(mktemp -d "$PWD/.cclsh-build-transaction.XXXXXX")
temporary_link=$build_transaction/kernel-link
ln -s "$release_directory/cclsh" "$temporary_link"
mv -Tf "$temporary_link" cclsh
temporary_link=
temporary_link=$build_transaction/image-link
ln -s "$release_directory/cclsh.image" "$temporary_link"
mv -Tf "$temporary_link" cclsh.image
temporary_link=
sync -f .
rm -rf -- "$build_transaction"
build_transaction=
rm -rf -- "$validation_home"
validation_home=
trap - 0 1 2 15
echo "Built ./cclsh release $release_id and ./cclsh.image"
