#!/usr/bin/env bash set -u ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" VENV_DIR="${HOME}/.local/share/ffx.venv" VENV_BIN_DIR="${VENV_DIR}/bin" VENV_PYTHON="${VENV_BIN_DIR}/python" VENV_PIP="${VENV_BIN_DIR}/pip" VENV_FFX="${VENV_BIN_DIR}/ffx" BASHRC_FILE="${HOME}/.bashrc" ALIAS_BLOCK_BEGIN="# >>> ffx alias >>>" ALIAS_BLOCK_END="# <<< ffx alias <<<" ALIAS_LINE="alias ffx=\"${VENV_FFX}\"" CHECK_ONLY=0 WITH_TESTS=0 READINESS_FAILURES=0 INSTALL_FAILURES=0 COLOR_RESET="" COLOR_GREEN="" COLOR_YELLOW="" COLOR_RED="" if [ -t 1 ]; then COLOR_RESET="$(printf '\033[0m')" COLOR_GREEN="$(printf '\033[32m')" COLOR_YELLOW="$(printf '\033[33m')" COLOR_RED="$(printf '\033[31m')" fi usage() { cat < ${VENV_FFX} - optionally install Python packages required for modern tests Options: --check Report readiness only. Do not create or modify anything. --with-tests Also install and verify Python packages required for modern tests. --help Show this help text. Notes: - This is the first installation step. - After the bundle is installed, the aligned CLI wrapper is: ffx setup - tools/configure_workstation.sh is the second step and configures system dependencies plus local user files. EOF } status_ok() { printf '%sok%s' "${COLOR_GREEN}" "${COLOR_RESET}" } status_warn() { printf '%swarn%s' "${COLOR_YELLOW}" "${COLOR_RESET}" } status_fail() { printf '%sfailed%s' "${COLOR_RED}" "${COLOR_RESET}" } report_component() { local level="$1" local label="$2" local detail="$3" local rendered_status="" case "${level}" in ok) rendered_status="$(status_ok)" ;; warn) rendered_status="$(status_warn)" ;; *) rendered_status="$(status_fail)" ;; esac printf '[%s] %s%s\n' "${rendered_status}" "${label}" "${detail:+: $detail}" } command_exists() { command -v "$1" >/dev/null 2>&1 } check_python3() { command_exists python3 } check_venv_dir() { [ -x "${VENV_PYTHON}" ] } check_venv_pip() { check_venv_dir && "${VENV_PIP}" --version >/dev/null 2>&1 } check_venv_ffx() { [ -x "${VENV_FFX}" ] } check_venv_pytest() { check_venv_dir && "${VENV_PYTHON}" -m pytest --version >/dev/null 2>&1 } check_bashrc_file() { [ -f "${BASHRC_FILE}" ] } check_bashrc_alias() { check_bashrc_file && grep -Fqx "${ALIAS_LINE}" "${BASHRC_FILE}" } detail_python3() { command -v python3 || printf "command 'python3' not found" } detail_venv_dir() { if check_venv_dir; then printf '%s' "${VENV_DIR}" else printf 'missing %s' "${VENV_DIR}" fi } detail_venv_pip() { if check_venv_pip; then "${VENV_PIP}" --version else printf 'missing pip in %s' "${VENV_DIR}" fi } detail_venv_ffx() { if check_venv_ffx; then printf '%s' "${VENV_FFX}" else printf 'missing %s' "${VENV_FFX}" fi } detail_venv_pytest() { if check_venv_pytest; then "${VENV_PYTHON}" -m pytest --version 2>/dev/null | head -n 1 else printf 'missing pytest in %s' "${VENV_DIR}" fi } detail_bashrc_file() { if check_bashrc_file; then printf '%s' "${BASHRC_FILE}" else printf 'missing %s; prep can create it' "${BASHRC_FILE}" fi } detail_bashrc_alias() { if check_bashrc_alias; then printf '%s' "${ALIAS_LINE}" else printf 'missing alias line for %s' "${VENV_FFX}" fi } print_status_report() { READINESS_FAILURES=0 echo "Dependency status:" if check_python3; then report_component ok "python3" "$(detail_python3)" else report_component failed "python3" "$(detail_python3)" READINESS_FAILURES=$((READINESS_FAILURES + 1)) fi echo echo "Bundle venv status:" if check_venv_dir; then report_component ok "bundle virtualenv" "$(detail_venv_dir)" else report_component failed "bundle virtualenv" "$(detail_venv_dir)" READINESS_FAILURES=$((READINESS_FAILURES + 1)) fi if check_venv_pip; then report_component ok "bundle pip" "$(detail_venv_pip)" else report_component failed "bundle pip" "$(detail_venv_pip)" READINESS_FAILURES=$((READINESS_FAILURES + 1)) fi if check_venv_ffx; then report_component ok "bundle ffx" "$(detail_venv_ffx)" else report_component failed "bundle ffx" "$(detail_venv_ffx)" READINESS_FAILURES=$((READINESS_FAILURES + 1)) fi if [ "${WITH_TESTS}" -eq 1 ]; then echo echo "Bundle test package status:" if check_venv_pytest; then report_component ok "bundle pytest" "$(detail_venv_pytest)" else report_component failed "bundle pytest" "$(detail_venv_pytest)" READINESS_FAILURES=$((READINESS_FAILURES + 1)) fi fi echo echo "Shell exposure status:" if check_bashrc_file; then report_component ok ".bashrc" "$(detail_bashrc_file)" else report_component warn ".bashrc" "$(detail_bashrc_file)" fi if check_bashrc_alias; then report_component ok "ffx alias" "$(detail_bashrc_alias)" else report_component failed "ffx alias" "$(detail_bashrc_alias)" READINESS_FAILURES=$((READINESS_FAILURES + 1)) fi } ensure_bundle_venv() { mkdir -p "${HOME}/.local/share" if ! check_venv_dir; then printf 'Creating bundle virtualenv at %s...\n' "${VENV_DIR}" if ! python3 -m venv "${VENV_DIR}"; then printf 'Failed to create virtualenv at %s.\n' "${VENV_DIR}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi fi if ! check_venv_pip; then printf 'Missing pip in %s.\n' "${VENV_DIR}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi if [ "${WITH_TESTS}" -eq 1 ]; then printf 'Installing FFX package and test extras into %s...\n' "${VENV_DIR}" if ! ( cd "${ROOT_DIR}" && "${VENV_PIP}" install --editable '.[test]' ); then printf 'Failed to install FFX package and test extras into %s.\n' "${VENV_DIR}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi else printf 'Installing FFX package into %s...\n' "${VENV_DIR}" if ! "${VENV_PIP}" install --editable "${ROOT_DIR}"; then printf 'Failed to install FFX package into %s.\n' "${VENV_DIR}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi fi return 0 } write_alias_block() { local bashrc_dir bashrc_dir="$(dirname "${BASHRC_FILE}")" mkdir -p "${bashrc_dir}" touch "${BASHRC_FILE}" if grep -Fq "${ALIAS_BLOCK_BEGIN}" "${BASHRC_FILE}" || grep -Fq "${ALIAS_BLOCK_END}" "${BASHRC_FILE}"; then if ! python3 - "${BASHRC_FILE}" "${ALIAS_BLOCK_BEGIN}" "${ALIAS_BLOCK_END}" "${ALIAS_LINE}" <<'PY' import pathlib import sys path = pathlib.Path(sys.argv[1]) begin = sys.argv[2] end = sys.argv[3] alias_line = sys.argv[4] content = path.read_text() block = f"{begin}\n{alias_line}\n{end}\n" start = content.find(begin) stop = content.find(end) if start != -1 and stop != -1 and stop >= start: stop += len(end) if stop < len(content) and content[stop] == "\n": stop += 1 content = content[:start] + block + content[stop:] else: if content and not content.endswith("\n"): content += "\n" content += block path.write_text(content) PY then printf 'Failed to update managed alias block in %s.\n' "${BASHRC_FILE}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 fi elif check_bashrc_alias; then : else { if [ -s "${BASHRC_FILE}" ] && [ "$(tail -c 1 "${BASHRC_FILE}" 2>/dev/null || true)" != "" ]; then printf '\n' fi printf '%s\n' "${ALIAS_BLOCK_BEGIN}" printf '%s\n' "${ALIAS_LINE}" printf '%s\n' "${ALIAS_BLOCK_END}" } >>"${BASHRC_FILE}" || { printf 'Failed to append alias block to %s.\n' "${BASHRC_FILE}" >&2 INSTALL_FAILURES=$((INSTALL_FAILURES + 1)) return 1 } fi return 0 } ensure_bashrc_alias() { printf 'Ensuring ffx alias in %s...\n' "${BASHRC_FILE}" write_alias_block } parse_args() { while [ "$#" -gt 0 ]; do case "$1" in --check) CHECK_ONLY=1 ;; --with-tests) WITH_TESTS=1 ;; --help|-h) usage exit 0 ;; *) printf 'Unknown option: %s\n\n' "$1" >&2 usage >&2 exit 2 ;; esac shift done } main() { parse_args "$@" print_status_report if [ "${CHECK_ONLY}" -eq 0 ]; then if ! check_python3; then printf '\npython3 is required before the bundle venv can be prepared.\n' >&2 exit 1 fi echo ensure_bundle_venv ensure_bashrc_alias echo print_status_report fi echo if [ "${INSTALL_FAILURES}" -gt 0 ]; then echo "One or more bundle preparation steps failed; see the status checks above." >&2 exit 1 fi if [ "${READINESS_FAILURES}" -gt 0 ]; then echo "The FFX bundle virtualenv and/or alias setup is incomplete." >&2 exit 1 fi echo "The FFX bundle virtualenv is ready." } main "$@"