feat(easy-docker): bootstrap jq dependency on startup

This commit is contained in:
RocketQuack 2026-04-20 23:26:57 +02:00
parent 6fdd9a3b84
commit 77a0f9e171
14 changed files with 668 additions and 0 deletions

View file

@ -0,0 +1,10 @@
# version asset_name sha256
1.8.1 jq-linux-amd64 020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d
1.8.1 jq-linux64 020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d
1.8.1 jq-linux-arm64 6bc62f25981328edd3cfcfe6fe51b073f2d7e7710d7ef7fcdac28d4e384fc3d4
1.8.1 jq-linux-armhf ac304e50cf7cd24933d83dc7d0e4f79892a71a92fb02336d4ecaffa8933760bd
1.8.1 jq-macos-amd64 e80dbe0d2a2597e3c11c404f03337b981d74b4a8504b70586c354b7697a7c27f
1.8.1 jq-osx-amd64 e80dbe0d2a2597e3c11c404f03337b981d74b4a8504b70586c354b7697a7c27f
1.8.1 jq-macos-arm64 a9fe3ea2f86dfc72f6728417521ec9067b343277152b114f4e98d8cb0e263603
1.8.1 jq-win64.exe 23cb60a1354eed6bcc8d9b9735e8c7b388cd1fdcb75726b93bc299ef22dd9334
1.8.1 jq-windows-amd64.exe 23cb60a1354eed6bcc8d9b9735e8c7b388cd1fdcb75726b93bc299ef22dd9334
1 # version asset_name sha256
2 1.8.1 jq-linux-amd64 020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d
3 1.8.1 jq-linux64 020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d
4 1.8.1 jq-linux-arm64 6bc62f25981328edd3cfcfe6fe51b073f2d7e7710d7ef7fcdac28d4e384fc3d4
5 1.8.1 jq-linux-armhf ac304e50cf7cd24933d83dc7d0e4f79892a71a92fb02336d4ecaffa8933760bd
6 1.8.1 jq-macos-amd64 e80dbe0d2a2597e3c11c404f03337b981d74b4a8504b70586c354b7697a7c27f
7 1.8.1 jq-osx-amd64 e80dbe0d2a2597e3c11c404f03337b981d74b4a8504b70586c354b7697a7c27f
8 1.8.1 jq-macos-arm64 a9fe3ea2f86dfc72f6728417521ec9067b343277152b114f4e98d8cb0e263603
9 1.8.1 jq-win64.exe 23cb60a1354eed6bcc8d9b9735e8c7b388cd1fdcb75726b93bc299ef22dd9334
10 1.8.1 jq-windows-amd64.exe 23cb60a1354eed6bcc8d9b9735e8c7b388cd1fdcb75726b93bc299ef22dd9334

View file

@ -0,0 +1,6 @@
#!/usr/bin/env bash
jq_check_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/easy-docker/lib/install/jq/load.sh
source "${jq_check_dir}/../install/jq/load.sh"

View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
get_easy_docker_jq_command() {
if command -v jq >/dev/null 2>&1; then
printf 'jq\n'
return 0
fi
if command -v jq.exe >/dev/null 2>&1; then
printf 'jq.exe\n'
return 0
fi
return 1
}
easy_docker_require_jq() {
get_easy_docker_jq_command >/dev/null 2>&1
}
easy_docker_run_jq() {
local jq_command=""
jq_command="$(get_easy_docker_jq_command)" || return 127
"${jq_command}" "$@"
}

View file

@ -27,3 +27,13 @@ print_docker_command_support_guidance() {
echo "Update Docker to a recent version and ensure Compose v2 is available as 'docker compose'."
echo "Standard 'docker' and 'docker compose' commands are required."
}
print_jq_install_guidance() {
print_manual_jq_install_guidance
}
print_manual_jq_install_guidance() {
echo "Install jq first: https://jqlang.org/download/"
echo "On Windows, you can also use: winget install jqlang.jq"
echo "Ensure the 'jq' or 'jq.exe' command is available on PATH before running easy-docker."
}

View file

@ -0,0 +1,144 @@
#!/usr/bin/env bash
get_jq_checksums_path() {
local jq_lib_dir=""
jq_lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
printf '%s/../../../config/jq-checksums.tsv\n' "${jq_lib_dir}"
}
get_pinned_jq_version() {
local checksums_path=""
local release_version=""
checksums_path="$(get_jq_checksums_path)"
if [ ! -f "${checksums_path}" ]; then
return 1
fi
release_version="$(
awk -F '\t' '
/^[[:space:]]*#/ { next }
NF < 3 { next }
{
print $1
exit
}
' "${checksums_path}"
)"
if [ -z "${release_version}" ]; then
return 1
fi
printf '%s\n' "${release_version}"
}
get_pinned_jq_asset_checksum() {
local release_version="${1}"
local asset_name="${2}"
local checksums_path=""
local expected_checksum=""
checksums_path="$(get_jq_checksums_path)"
if [ ! -f "${checksums_path}" ]; then
return 1
fi
expected_checksum="$(
awk -F '\t' -v release_version="${release_version}" -v asset_name="${asset_name}" '
/^[[:space:]]*#/ { next }
NF < 3 { next }
$1 == release_version && $2 == asset_name {
print $3
exit
}
' "${checksums_path}"
)"
if [ -z "${expected_checksum}" ]; then
return 1
fi
printf '%s\n' "${expected_checksum}"
}
sha256_verification_available_for_jq() {
command_exists sha256sum ||
command_exists shasum ||
command_exists openssl ||
command_exists certutil
}
compute_file_sha256_for_jq() {
local file_path="${1}"
local hash_input_path="${file_path}"
if command_exists sha256sum; then
sha256sum "${file_path}" | awk '{print tolower($1)}'
return $?
fi
if command_exists shasum; then
shasum -a 256 "${file_path}" | awk '{print tolower($1)}'
return $?
fi
if command_exists openssl; then
openssl dgst -sha256 -r "${file_path}" | awk '{print tolower($1)}'
return $?
fi
if command_exists certutil; then
if command_exists cygpath; then
hash_input_path="$(cygpath -w "${file_path}" 2>/dev/null || printf '%s' "${file_path}")"
fi
certutil -hashfile "${hash_input_path}" SHA256 2>/dev/null |
awk 'NR == 2 { gsub(/ /, "", $0); print tolower($0); exit }'
return $?
fi
return 1
}
verify_file_sha256_for_jq() {
local file_path="${1}"
local expected_checksum="${2}"
local actual_checksum=""
actual_checksum="$(compute_file_sha256_for_jq "${file_path}" || true)"
if [ -z "${actual_checksum}" ]; then
return 1
fi
if [ "${actual_checksum}" != "$(printf '%s' "${expected_checksum}" | tr '[:upper:]' '[:lower:]')" ]; then
return 1
fi
return 0
}
get_jq_asset_candidates() {
local jq_os="${1}"
local jq_arch="${2}"
case "${jq_os}:${jq_arch}" in
linux:amd64)
printf '%s\n%s\n' "jq-linux-amd64" "jq-linux64"
;;
linux:arm64)
printf '%s\n' "jq-linux-arm64"
;;
linux:armhf)
printf '%s\n' "jq-linux-armhf"
;;
macos:amd64)
printf '%s\n%s\n' "jq-macos-amd64" "jq-osx-amd64"
;;
macos:arm64)
printf '%s\n' "jq-macos-arm64"
;;
windows:amd64)
printf '%s\n%s\n' "jq-windows-amd64.exe" "jq-win64.exe"
;;
esac
}

View file

@ -0,0 +1,67 @@
#!/usr/bin/env bash
should_use_jq_github_fallback() {
local answer=""
if [ ! -t 0 ]; then
echo "GitHub fallback prompt requires an interactive terminal."
return 1
fi
printf "Use GitHub binary fallback for jq? [y/N]: "
read -r answer
case "${answer}" in
y | Y | yes | YES)
return 0
;;
*)
return 1
;;
esac
}
ensure_jq() {
local disable_installation_fallback="${1:-0}"
if get_easy_docker_jq_command >/dev/null 2>&1; then
return 0
fi
echo "jq is not installed. Trying package manager installation..."
if install_jq_with_package_manager; then
hash -r
fi
if get_easy_docker_jq_command >/dev/null 2>&1; then
echo "jq was installed successfully."
return 0
fi
if [ "${disable_installation_fallback}" = "1" ]; then
echo "Installation fallback is disabled."
print_manual_jq_install_guidance
return 1
fi
if should_use_jq_github_fallback; then
echo "Trying pinned GitHub release fallback..."
if install_jq_from_github_release; then
hash -r
fi
else
echo "GitHub fallback was not selected."
print_manual_jq_install_guidance
return 1
fi
if get_easy_docker_jq_command >/dev/null 2>&1; then
echo "jq was installed successfully."
return 0
fi
echo "Automatic installation failed."
print_manual_jq_install_guidance
return 1
}

View file

@ -0,0 +1,109 @@
#!/usr/bin/env bash
cleanup_jq_tmp_dir() {
local tmp_dir="${1:-}"
if [ -n "${tmp_dir}" ] && [ -d "${tmp_dir}" ]; then
rm -rf "${tmp_dir}"
fi
}
install_jq_from_github_release() {
local release_version=""
local checksums_path=""
local jq_os=""
local jq_arch=""
local asset_name=""
local asset_path=""
local download_url=""
local tmp_dir=""
local target_dir=""
local target_binary_name="jq"
local expected_checksum=""
if ! command_exists curl; then
echo "curl is required for the GitHub fallback."
return 1
fi
if ! read -r jq_os jq_arch < <(detect_jq_platform); then
echo "Unsupported platform for automatic GitHub fallback."
return 1
fi
release_version="$(get_pinned_jq_version || true)"
if [ -z "${release_version}" ]; then
echo "Could not determine the pinned jq release version."
return 1
fi
checksums_path="$(get_jq_checksums_path)"
if [ ! -f "${checksums_path}" ]; then
echo "Pinned jq checksum file is missing: ${checksums_path}"
return 1
fi
if ! sha256_verification_available_for_jq; then
echo "A SHA256 verification tool is required for the GitHub fallback."
return 1
fi
tmp_dir="$(mktemp -d 2>/dev/null || true)"
if [ -z "${tmp_dir}" ] || [ ! -d "${tmp_dir}" ]; then
echo "Failed to create temporary directory for jq installation."
return 1
fi
while IFS= read -r asset_name; do
expected_checksum="$(get_pinned_jq_asset_checksum "${release_version}" "${asset_name}" || true)"
if [ -z "${expected_checksum}" ]; then
continue
fi
asset_path="${tmp_dir}/${asset_name}"
download_url="https://github.com/jqlang/jq/releases/download/jq-${release_version}/${asset_name}"
if ! curl -fsSL "${download_url}" -o "${asset_path}"; then
continue
fi
if ! verify_file_sha256_for_jq "${asset_path}" "${expected_checksum}"; then
echo "Checksum verification failed for ${asset_name}."
continue
fi
if [[ "${asset_name}" == *.exe ]]; then
target_binary_name="jq.exe"
else
target_binary_name="jq"
fi
if [ "${jq_os}" != "windows" ] && [ -w "/usr/local/bin" ]; then
target_dir="/usr/local/bin"
if copy_binary "${asset_path}" "${target_dir}/${target_binary_name}"; then
cleanup_jq_tmp_dir "${tmp_dir}"
return 0
fi
fi
if [ "${jq_os}" != "windows" ] && command_exists sudo; then
if copy_binary_with_privileges "${asset_path}" "/usr/local/bin/${target_binary_name}"; then
cleanup_jq_tmp_dir "${tmp_dir}"
return 0
fi
fi
if [ -n "${HOME:-}" ]; then
target_dir="${HOME}/.local/bin"
mkdir -p "${target_dir}"
if copy_binary "${asset_path}" "${target_dir}/${target_binary_name}"; then
cleanup_jq_tmp_dir "${tmp_dir}"
return 0
fi
fi
done < <(get_jq_asset_candidates "${jq_os}" "${jq_arch}")
cleanup_jq_tmp_dir "${tmp_dir}"
echo "No compatible, verified jq binary was installed from the pinned GitHub release."
return 1
}

View file

@ -0,0 +1,20 @@
#!/usr/bin/env bash
load_jq_install_modules() {
local jq_lib_dir=""
jq_lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/easy-docker/lib/install/jq/platform.sh
source "${jq_lib_dir}/platform.sh"
# shellcheck source=scripts/easy-docker/lib/install/jq/assets.sh
source "${jq_lib_dir}/assets.sh"
# shellcheck source=scripts/easy-docker/lib/install/jq/package_manager.sh
source "${jq_lib_dir}/package_manager.sh"
# shellcheck source=scripts/easy-docker/lib/install/jq/github_release.sh
source "${jq_lib_dir}/github_release.sh"
# shellcheck source=scripts/easy-docker/lib/install/jq/ensure.sh
source "${jq_lib_dir}/ensure.sh"
}
load_jq_install_modules

View file

@ -0,0 +1,62 @@
#!/usr/bin/env bash
install_jq_with_package_manager() {
local pm_attempted=0
if command_exists brew; then
pm_attempted=1
if brew install jq; then
return 0
fi
fi
if command_exists apt-get; then
pm_attempted=1
if run_with_privileges apt-get update && run_with_privileges apt-get install -y jq; then
return 0
fi
fi
if command_exists dnf; then
pm_attempted=1
if run_with_privileges dnf install -y jq; then
return 0
fi
fi
if command_exists pacman; then
pm_attempted=1
if run_with_privileges pacman -Sy --noconfirm jq; then
return 0
fi
fi
if command_exists zypper; then
pm_attempted=1
if run_with_privileges zypper --non-interactive install jq; then
return 0
fi
fi
if command_exists winget; then
pm_attempted=1
if winget install --id jqlang.jq -e --accept-source-agreements --accept-package-agreements; then
return 0
fi
fi
if command_exists choco; then
pm_attempted=1
if choco install jq -y; then
return 0
fi
fi
if [ "${pm_attempted}" -eq 0 ]; then
echo "No supported package manager was found."
else
echo "Package manager installation did not succeed."
fi
return 1
}

View file

@ -0,0 +1,52 @@
#!/usr/bin/env bash
detect_jq_platform() {
local raw_os=""
local raw_arch=""
local jq_os=""
local jq_arch=""
raw_os="$(uname -s 2>/dev/null || echo unknown)"
raw_arch="$(uname -m 2>/dev/null || echo unknown)"
case "${raw_os}" in
Linux*)
jq_os="linux"
;;
Darwin*)
jq_os="macos"
;;
MINGW* | MSYS* | CYGWIN* | Windows_NT)
jq_os="windows"
;;
*)
return 1
;;
esac
case "${raw_arch}" in
x86_64 | amd64)
jq_arch="amd64"
;;
aarch64 | arm64)
jq_arch="arm64"
;;
armv7l | armv7)
jq_arch="armhf"
;;
*)
return 1
;;
esac
if [ "${jq_os}" = "windows" ] && [ "${jq_arch}" != "amd64" ]; then
return 1
fi
if [ "${jq_os}" = "macos" ] && [ "${jq_arch}" = "armhf" ]; then
return 1
fi
printf '%s %s\n' "${jq_os}" "${jq_arch}"
return 0
}

View file

@ -8,10 +8,14 @@ load_easy_docker_modules() {
source "${lib_dir}/core/commands.sh"
# shellcheck source=scripts/easy-docker/lib/core/messages.sh
source "${lib_dir}/core/messages.sh"
# shellcheck source=scripts/easy-docker/lib/core/json.sh
source "${lib_dir}/core/json.sh"
# shellcheck source=scripts/easy-docker/lib/install/gum/load.sh
source "${lib_dir}/install/gum/load.sh"
# shellcheck source=scripts/easy-docker/lib/checks/docker.sh
source "${lib_dir}/checks/docker.sh"
# shellcheck source=scripts/easy-docker/lib/checks/jq.sh
source "${lib_dir}/checks/jq.sh"
# shellcheck source=scripts/easy-docker/lib/ui/screens.sh
source "${lib_dir}/ui/screens.sh"
# shellcheck source=scripts/easy-docker/lib/app/screen.sh

View file

@ -25,6 +25,10 @@ if ! ensure_docker; then
exit 1
fi
if ! ensure_jq "${disable_installation_fallback}"; then
exit 1
fi
trap 'leave_alt_screen; exit 0' INT TERM
trap 'leave_alt_screen' EXIT

View file

@ -84,3 +84,23 @@ write_passthrough_stub() {
[[ "${output}" == *"docker is not installed."* ]]
[[ "${output}" == *"Install Docker first:"* ]]
}
@test "missing jq stops after gum and docker dependencies succeed" {
write_stub gum 'exit 0'
# shellcheck disable=SC2016
write_stub docker \
'if [ "${1:-}" = "compose" ] && [ "${2:-}" = "version" ]; then exit 0; fi' \
'if [ "${1:-}" = "info" ]; then exit 0; fi' \
'case "$*" in' \
' "ps --help"| "exec --help"| "inspect --help"| "cp --help"| "build --help"| "compose config --help"| "compose up --help"| "compose down --help"| "compose logs --help"| "compose exec --help"| "compose pull --help"| "compose ps --help") exit 0 ;;' \
'esac' \
'exit 0'
run "${SYSTEM_ENV}" "PATH=${STUB_BIN}" "${SYSTEM_BASH}" "${MAIN_SCRIPT}" --no-installation-fallback
[ "${status}" -eq 1 ]
[[ "${output}" == *"jq is not installed. Trying package manager installation..."* ]]
[[ "${output}" == *"No supported package manager was found."* ]]
[[ "${output}" == *"Installation fallback is disabled."* ]]
[[ "${output}" == *"Install jq first:"* ]]
}

View file

@ -0,0 +1,134 @@
#!/usr/bin/env bats
load 'test_helper.bash'
setup() {
easy_docker_test_begin
easy_docker_test_source_jq_modules
}
teardown() {
easy_docker_test_end
}
@test "ensure_jq fails when jq is not installed" {
# shellcheck disable=SC2317
get_easy_docker_jq_command() {
return 1
}
# shellcheck disable=SC2317
install_jq_with_package_manager() {
echo "No supported package manager was found."
return 1
}
run ensure_jq 1
[ "${status}" -eq 1 ]
[[ "${output}" == *"jq is not installed. Trying package manager installation..."* ]]
[[ "${output}" == *"No supported package manager was found."* ]]
[[ "${output}" == *"Installation fallback is disabled."* ]]
[[ "${output}" == *"Install jq first:"* ]]
}
@test "ensure_jq succeeds when jq is installed" {
# shellcheck disable=SC2317
get_easy_docker_jq_command() {
printf '%s\n' "jq"
}
run ensure_jq 0
[ "${status}" -eq 0 ]
[ -z "${output}" ]
}
@test "ensure_jq succeeds when only jq.exe is installed" {
# shellcheck disable=SC2317
get_easy_docker_jq_command() {
printf '%s\n' "jq.exe"
}
run ensure_jq 0
[ "${status}" -eq 0 ]
[ -z "${output}" ]
}
@test "should_use_jq_github_fallback rejects non-interactive terminals" {
local script_path=""
local repo_root=""
repo_root="$(easy_docker_test_repo_root)"
script_path="${EASY_DOCKER_TEST_TMPDIR}/run-should-use-jq-github-fallback"
easy_docker_test_write_executable "${script_path}" \
"source \"${repo_root}/scripts/easy-docker/lib/install/jq/ensure.sh\"" \
'should_use_jq_github_fallback'
run "${script_path}" </dev/null
[ "${status}" -eq 1 ]
[[ "${output}" == *"GitHub fallback prompt requires an interactive terminal."* ]]
}
@test "ensure_jq succeeds when github fallback installs jq" {
jq_installed=0
# shellcheck disable=SC2317
get_easy_docker_jq_command() {
if [ "${jq_installed}" -eq 1 ]; then
printf '%s\n' "jq"
return 0
fi
return 1
}
# shellcheck disable=SC2317
install_jq_with_package_manager() {
echo "Package manager installation did not succeed."
return 1
}
# shellcheck disable=SC2317
should_use_jq_github_fallback() {
return 0
}
# shellcheck disable=SC2317
install_jq_from_github_release() {
jq_installed=1
return 0
}
run ensure_jq 0
[ "${status}" -eq 0 ]
[[ "${output}" == *"Trying pinned GitHub release fallback..."* ]]
[[ "${output}" == *"jq was installed successfully."* ]]
}
@test "ensure_jq aborts when github fallback is declined" {
# shellcheck disable=SC2317
get_easy_docker_jq_command() {
return 1
}
# shellcheck disable=SC2317
install_jq_with_package_manager() {
echo "No supported package manager was found."
return 1
}
# shellcheck disable=SC2317
should_use_jq_github_fallback() {
return 1
}
run ensure_jq 0
[ "${status}" -eq 1 ]
[[ "${output}" == *"GitHub fallback was not selected."* ]]
[[ "${output}" == *"Install jq first:"* ]]
}