diff --git a/README.md b/README.md index b6a3826..9bbc83f 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ replacement, but a wrapper. - Dry-run mode for reviewing planned work before changing a system. - Explain mode for understanding how manifest entries become planned actions. - Native package-manager delegation for APT, Alpine APK, and DNF. +- Visible per-package installation progress outside quiet mode. +- Per-package installation timeouts when GNU `timeout` is available. +- APT installations that omit automatically recommended packages. - Conservative diagnostics that stop rather than guessing when input is unclear. - A single generated `bootstrap.bash` release artifact. - Support for multiple system package managers: diff --git a/doc/adr/ADR-050-Bound-Package-Installation-and-Report-Progress.md b/doc/adr/ADR-050-Bound-Package-Installation-and-Report-Progress.md new file mode 100644 index 0000000..ccae836 --- /dev/null +++ b/doc/adr/ADR-050-Bound-Package-Installation-and-Report-Progress.md @@ -0,0 +1,193 @@ +# ADR-050: Bound Package Installation and Report Progress + +Date: 2026-07-11 + +## Status + +Accepted + +## Context + +Bootstrap delegates package installation to the native APT, APK, and DNF +package managers. That delegation preserves native package-manager authority, +but it also means Bootstrap inherits package-manager behavior that may wait for +interactive input or otherwise fail to return. + +Issue 42 records an APT installation of `chkrootkit` whose recommended +dependencies caused `dpkg` to attempt interaction during a non-interactive +Bootstrap run. The process appeared to hang indefinitely. Bootstrap also +suppressed the native package-manager output and printed no package-level +progress, so the user could not distinguish a slow successful installation from +an installation waiting forever. + +Three separate concerns are present: + +1. APT may install recommended packages that were not explicitly requested and + that may introduce interactive behavior. +2. Any supported package-manager installation may fail to return. +3. Suppressed output leaves the user without visible evidence that installation + work is still in progress. + +Bootstrap should reduce the known APT trigger, bound package installation when +the host provides the necessary capability, and report visible progress without +corrupting the structured Execution Result stream. + +## Decision Drivers + +- Package installation should not wait indefinitely when a standard timeout + facility is available. +- Minimal supported systems should remain usable even when GNU `timeout` is not + installed. +- A degraded safety boundary must never be hidden from the user. +- APT should install the explicitly requested package without automatically + expanding the operation to recommended packages. +- Users should be able to distinguish active installation from an apparent hang. +- Quiet mode may suppress routine progress, but it must not suppress warnings, + errors, or recovery guidance. +- Executor standard output must remain reserved for structured Execution Result + records. +- The implementation should remain small, deterministic, and shared across APT, + APK, and DNF where their behavior is equivalent. + +## Decision + +APT package installation shall use `--no-install-recommends`. + +Each mutating APT, APK, and DNF package installation shall be run through GNU +`timeout` when that command is available. The timeout applies separately to +each package installation. Its duration is configured by +`BOOTSTRAP_INSTALL_TIMEOUT`, measured in seconds, with a default value of `30`. +The effective value must be a positive whole number. Invalid values are usage +errors discovered before execution begins. + +`timeout` is an optional runtime capability rather than a mandatory dependency. +Bootstrap shall check for it before attempting a package installation. When it +is unavailable, Bootstrap shall emit one warning per invocation and continue +with unbounded native package-manager installations. Quiet mode shall not +suppress this warning because it describes a degraded safety mechanism. + +A timeout shall produce a failed Execution Result and the existing execution +failure exit category. Bootstrap shall not introduce a new public exit code for +this condition. Package-state inspection commands are outside this timeout +boundary. + +Immediately before a package installation, Bootstrap shall print the following +progress prefix to standard error when quiet mode is not active: + +```text +Installing PACKAGE... +``` + +The text shall remain on one logical line and shall be completed with `done.` on +success or `failed.` on failure. Already-satisfied packages shall not print an +installation progress message. Progress belongs on standard error because +executor standard output carries machine-readable Execution Result records. + +Native package-manager output may remain suppressed. Recovery guidance shall +show the native command a user can run directly for complete diagnostics. The +APT recovery command shall include `--no-install-recommends`; recovery examples +need not include Bootstrap's timeout wrapper so users may diagnose failures +interactively and deliberately. + +## Considered Alternatives + +### Add only `--no-install-recommends` + +This addresses the known APT trigger but does not protect APK, DNF, or unrelated +APT failures that never return. It also leaves the user without progress +information. + +### Rely only on `DEBIAN_FRONTEND=noninteractive` + +That variable can reduce some Debian-family prompts, but it neither prevents all +blocking behavior nor applies to APK and DNF. It is not a general execution +boundary. + +### Require `timeout` and stop when it is missing + +This provides the strongest uniform guarantee, but it prevents Bootstrap from +running on otherwise supported minimal systems. Bootstrap instead continues +with an explicit warning so compatibility is preserved without hiding the +risk. + +### Silently continue without `timeout` + +This preserves compatibility but falsely implies that the documented safety +boundary is active. Silent degradation is inconsistent with Bootstrap's +human-centered diagnostics and conservative behavior. + +### Apply one timeout to the entire Bootstrap invocation + +A run-level timeout would make the permitted duration depend on manifest size +and could interrupt planning, reporting, or several legitimate installations. +A per-package timeout gives each native installation the same explicit bound. + +### Show progress only in verbose mode + +The original failure is confusing during normal operation. Installation +progress is useful default feedback and should disappear only when the user +explicitly requests quiet output. + +### Expose native package-manager output + +This would provide activity feedback, but it would also make output noisy and +backend-dependent. Small stable progress messages preserve a consistent user +experience while recovery guidance provides a route to native diagnostics. + +### Introduce a timeout-specific public exit code + +The distinction is useful inside the Execution Result message, but callers already +have an execution-failure category. A new public code would expand the stable +interface without a demonstrated need. + +## Consequences + +### Positive + +- APT no longer installs recommended packages automatically. +- Package installations are time-bounded on systems that provide GNU `timeout`. +- Minimal systems without `timeout` remain supported. +- Users are explicitly warned when the timeout safety boundary is unavailable. +- Default progress makes slow installation visibly different from silence. +- Quiet mode continues to suppress routine progress without hiding degraded + safety, errors, or recovery guidance. +- A shared timeout and progress boundary keeps backend behavior consistent. + +### Negative + +- Systems without `timeout` remain vulnerable to indefinitely blocked native + package-manager commands after the warning is displayed. +- A 30-second default may be too short for unusually slow repositories or + systems, requiring an environment or `.env` override. +- Excluding APT recommendations may omit packages some users previously received + implicitly. +- Progress emitted on standard error becomes part of the observable CLI + contract. +- A package manager that independently exits with status 124 may be reported as + a timeout, matching GNU `timeout`'s conventional status. + +## Open Questions and Follow-Ups + +- Operational experience may justify changing the default timeout in a future + compatible release. +- Future package-manager backends should explicitly decide whether their + mutating installation command uses the shared timeout boundary. +- Future work may add a deliberate option to require timeout support rather than + accepting degraded execution. + +## Related Decisions + +- Related to: ADR-003 Treat Native Package Managers as the Source of Truth +- Related to: ADR-013 Fail Conservatively and Avoid Surprising System Changes +- Related to: ADR-017 Delegate Package Operations to Native Package Managers +- Related to: ADR-020 Provide Human-Centered Diagnostics +- Related to: ADR-025 Provide Human-Centered Logging with Progressive Levels of Detail +- Related to: ADR-026 Define a Stable Exit Code Philosophy +- Related to: ADR-028 Favor the Principle of Least Surprise +- Related to: ADR-039 Test Observable Behavior Rather Than Implementation +- Related to: ADR-040 Prefer Deterministic Behavior +- Related to: ADR-041 Treat Documentation as Part of the Product +- Related to: ADR-042 Minimize the Trusted Computing Base +- Related to: ADR-045 Documentation-First Source-Code Commenting Standard +- Related to: ADR-046 Adopt Documentation-Driven Test-Second Development +- Related to: ADR-048 Execution Shall Consume Only Resolved Actions diff --git a/doc/adr/README.md b/doc/adr/README.md index 8309e42..1ded22c 100644 --- a/doc/adr/README.md +++ b/doc/adr/README.md @@ -73,7 +73,7 @@ Topics include: These ADRs describe how the project should evolve over time. -- ADR-031 through ADR-044 +- ADR-031 through ADR-050 Topics include: @@ -84,7 +84,8 @@ Topics include: - determinism; - documentation; - trusted computing base; -- contributor philosophy. +- contributor philosophy; +- bounded package installation and visible progress. ## Architectural Themes diff --git a/doc/cli.md b/doc/cli.md index f5074e3..425366f 100644 --- a/doc/cli.md +++ b/doc/cli.md @@ -114,6 +114,50 @@ specified at most once per invocation. Because its source name is `-`, users who need per-file provenance should pass manifest paths instead of concatenating files into standard input. + +## Package installation timeout + +Bootstrap bounds each mutating APT, APK, and DNF package installation when GNU +`timeout` is available. The timeout applies separately to each package rather +than to the complete manifest or Bootstrap invocation. + +The duration is configured in seconds with `BOOTSTRAP_INSTALL_TIMEOUT`: + +```dotenv +BOOTSTRAP_INSTALL_TIMEOUT=30 +``` + +The built-in default is 30 seconds. The effective value must be a positive +whole number. Empty, zero, negative, fractional, and nonnumeric values are +usage errors discovered before package execution begins. + +`timeout` is an optional runtime capability. When it is unavailable, Bootstrap +prints one warning for the invocation and continues with unbounded native +package-manager installations. The warning remains visible under `--quiet` +because it identifies a degraded safety boundary. + +A package that exceeds the configured duration produces a failed Execution +Result and Bootstrap's existing execution-failure exit status. Package-state +inspection is not included in the timeout boundary. + +APT installations use `--no-install-recommends`, so packages classified only as +recommended dependencies are not installed automatically. + +## Package installation progress + +Immediately before installing a missing package, Bootstrap writes a progress +message to standard error: + +```text +Installing curl...done. +``` + +A failed or timed-out installation completes the same line with `failed.` before +printing its normal failure and recovery information. Already-installed +packages do not produce an installation progress message. `--quiet` suppresses +this routine progress output, while warnings, errors, and recovery guidance +remain visible. + ## `--help` `--help` prints a short usage summary and exits successfully. diff --git a/lib/executor/apk.bash b/lib/executor/apk.bash index edad51b..389d40e 100644 --- a/lib/executor/apk.bash +++ b/lib/executor/apk.bash @@ -84,7 +84,10 @@ bootstrap_executor_apk_install_package() { return "${BOOTSTRAP_EXIT_SUCCESS}" fi - if bootstrap_privilege_run apk add "${package}" >/dev/null; then + bootstrap_log_install_start "${package}" + + if bootstrap_privilege_run_install apk add "${package}" >/dev/null; then + bootstrap_log_install_done bootstrap_execution_result_create \ 'success' \ "${BOOTSTRAP_EXIT_SUCCESS}" \ @@ -95,6 +98,19 @@ bootstrap_executor_apk_install_package() { return "${BOOTSTRAP_EXIT_SUCCESS}" else status="$?" + bootstrap_log_install_failed + if [[ "${status}" == "124" ]]; then + bootstrap_execution_result_create \ + 'failed' \ + "${BOOTSTRAP_EXIT_EXECUTION}" \ + 'install-package' \ + 'apk' \ + "${package}" \ + "package installation timed out after $(bootstrap_context_get_install_timeout) seconds" + bootstrap_recovery_execution_failed 'apk' "${package}" + return "${BOOTSTRAP_EXIT_EXECUTION}" + fi + if [[ "${status}" == "${BOOTSTRAP_EXIT_PRIVILEGE}" ]]; then bootstrap_execution_result_create \ 'failed' \ diff --git a/lib/executor/apt.bash b/lib/executor/apt.bash index 52b2784..75b278c 100644 --- a/lib/executor/apt.bash +++ b/lib/executor/apt.bash @@ -85,7 +85,10 @@ bootstrap_executor_apt_install_package() { return "${BOOTSTRAP_EXIT_SUCCESS}" fi - if bootstrap_privilege_run apt-get install -y "${package}" >/dev/null; then + bootstrap_log_install_start "${package}" + + if bootstrap_privilege_run_install apt-get install -y --no-install-recommends "${package}" >/dev/null; then + bootstrap_log_install_done bootstrap_execution_result_create \ 'success' \ "${BOOTSTRAP_EXIT_SUCCESS}" \ @@ -96,6 +99,19 @@ bootstrap_executor_apt_install_package() { return "${BOOTSTRAP_EXIT_SUCCESS}" else status="$?" + bootstrap_log_install_failed + if [[ "${status}" == "124" ]]; then + bootstrap_execution_result_create \ + 'failed' \ + "${BOOTSTRAP_EXIT_EXECUTION}" \ + 'install-package' \ + 'apt' \ + "${package}" \ + "package installation timed out after $(bootstrap_context_get_install_timeout) seconds" + bootstrap_recovery_execution_failed 'apt' "${package}" + return "${BOOTSTRAP_EXIT_EXECUTION}" + fi + if [[ "${status}" == "${BOOTSTRAP_EXIT_PRIVILEGE}" ]]; then bootstrap_execution_result_create \ 'failed' \ diff --git a/lib/executor/dnf.bash b/lib/executor/dnf.bash index a36fa87..d0c7a06 100644 --- a/lib/executor/dnf.bash +++ b/lib/executor/dnf.bash @@ -84,7 +84,10 @@ bootstrap_executor_dnf_install_package() { return "${BOOTSTRAP_EXIT_SUCCESS}" fi - if bootstrap_privilege_run dnf install -y "${package}" >/dev/null; then + bootstrap_log_install_start "${package}" + + if bootstrap_privilege_run_install dnf install -y "${package}" >/dev/null; then + bootstrap_log_install_done bootstrap_execution_result_create \ 'success' \ "${BOOTSTRAP_EXIT_SUCCESS}" \ @@ -95,6 +98,19 @@ bootstrap_executor_dnf_install_package() { return "${BOOTSTRAP_EXIT_SUCCESS}" else status="$?" + bootstrap_log_install_failed + if [[ "${status}" == "124" ]]; then + bootstrap_execution_result_create \ + 'failed' \ + "${BOOTSTRAP_EXIT_EXECUTION}" \ + 'install-package' \ + 'dnf' \ + "${package}" \ + "package installation timed out after $(bootstrap_context_get_install_timeout) seconds" + bootstrap_recovery_execution_failed 'dnf' "${package}" + return "${BOOTSTRAP_EXIT_EXECUTION}" + fi + if [[ "${status}" == "${BOOTSTRAP_EXIT_PRIVILEGE}" ]]; then bootstrap_execution_result_create \ 'failed' \ diff --git a/lib/runtime/config.bash b/lib/runtime/config.bash index 02a3e23..d9645ae 100644 --- a/lib/runtime/config.bash +++ b/lib/runtime/config.bash @@ -125,6 +125,9 @@ bootstrap_config_apply_assignment() { BOOTSTRAP_PACKAGE_MANAGER) bootstrap_context_set_package_manager "${value}" ;; + BOOTSTRAP_INSTALL_TIMEOUT) + bootstrap_context_set_install_timeout "${value}" + ;; BOOTSTRAP_*) if [[ -n "${line_number}" ]]; then bootstrap_print_usage_error \ @@ -244,6 +247,10 @@ bootstrap_config_apply_environment() { if [[ -v BOOTSTRAP_PACKAGE_MANAGER ]]; then bootstrap_context_set_package_manager "${BOOTSTRAP_PACKAGE_MANAGER}" fi + + if [[ -v BOOTSTRAP_INSTALL_TIMEOUT ]]; then + bootstrap_context_set_install_timeout "${BOOTSTRAP_INSTALL_TIMEOUT}" + fi } ## @fn bootstrap_config_validate_package_manager() @@ -296,6 +303,15 @@ bootstrap_config_validate_package_manager() { ## bootstrap_config_validate_effective_runtime ## @endcode bootstrap_config_validate_effective_runtime() { + local timeout + bootstrap_config_validate_package_manager \ - "$(bootstrap_context_get_package_manager)" + "$(bootstrap_context_get_package_manager)" || return "$?" + + timeout="$(bootstrap_context_get_install_timeout)" + if [[ ! "${timeout}" =~ ^[1-9][0-9]*$ ]]; then + bootstrap_print_usage_error \ + "BOOTSTRAP_INSTALL_TIMEOUT must be a positive whole number of seconds: ${timeout:-}" + return "${BOOTSTRAP_EXIT_USAGE}" + fi } diff --git a/lib/runtime/context.bash b/lib/runtime/context.bash index f0e96ff..71126ab 100644 --- a/lib/runtime/context.bash +++ b/lib/runtime/context.bash @@ -42,6 +42,14 @@ BOOTSTRAP_MANIFEST_PATHS=() ## Effective package-manager selector after defaults and configuration are applied. BOOTSTRAP_CONTEXT_PACKAGE_MANAGER="auto" +## @var BOOTSTRAP_CONTEXT_INSTALL_TIMEOUT +## Per-package installation timeout in seconds. +BOOTSTRAP_CONTEXT_INSTALL_TIMEOUT="30" + +## @var BOOTSTRAP_TIMEOUT_WARNING_EMITTED +## True after the missing-timeout warning has been emitted for this invocation. +BOOTSTRAP_TIMEOUT_WARNING_EMITTED=false + ## @fn bootstrap_context_reset() ## @brief Restores runtime option state to documented defaults. ## @details @@ -62,6 +70,8 @@ bootstrap_context_reset() { BOOTSTRAP_FLAG_QUIET=false BOOTSTRAP_MANIFEST_PATHS=() BOOTSTRAP_CONTEXT_PACKAGE_MANAGER="auto" + BOOTSTRAP_CONTEXT_INSTALL_TIMEOUT="30" + BOOTSTRAP_TIMEOUT_WARNING_EMITTED=false } ## @fn bootstrap_context_enable_dry_run() @@ -248,3 +258,37 @@ bootstrap_context_is_verbose() { bootstrap_context_is_quiet() { [[ "${BOOTSTRAP_FLAG_QUIET}" == true ]] } + + +## @fn bootstrap_context_set_install_timeout() +## @brief Records the per-package installation timeout in seconds. +## @param seconds Positive whole-number timeout value. +## @retval 0 Timeout state was recorded successfully. +bootstrap_context_set_install_timeout() { + BOOTSTRAP_CONTEXT_INSTALL_TIMEOUT="$1" +} + +## @fn bootstrap_context_get_install_timeout() +## @brief Prints the effective per-package installation timeout. +## @par Standard Output +## Timeout duration in seconds. +## @retval 0 The timeout was printed successfully. +bootstrap_context_get_install_timeout() { + printf '%s +' "${BOOTSTRAP_CONTEXT_INSTALL_TIMEOUT}" +} + +## @fn bootstrap_context_timeout_warning_was_emitted() +## @brief Tests whether the degraded timeout warning was already printed. +## @retval 0 The warning was already printed. +## @retval 1 The warning has not been printed. +bootstrap_context_timeout_warning_was_emitted() { + [[ "${BOOTSTRAP_TIMEOUT_WARNING_EMITTED}" == true ]] +} + +## @fn bootstrap_context_mark_timeout_warning_emitted() +## @brief Records that the degraded timeout warning has been printed. +## @retval 0 Warning state was recorded successfully. +bootstrap_context_mark_timeout_warning_emitted() { + BOOTSTRAP_TIMEOUT_WARNING_EMITTED=true +} diff --git a/lib/runtime/logging.bash b/lib/runtime/logging.bash index 2215515..03630ff 100644 --- a/lib/runtime/logging.bash +++ b/lib/runtime/logging.bash @@ -138,3 +138,46 @@ bootstrap_log_error() { bootstrap_log_emit 'error' "${message}" 'stderr' } + + +## @fn bootstrap_log_install_start() +## @brief Prints an installation progress prefix unless quiet mode is active. +## @param package Package being installed. +## @par Standard Error +## An unterminated progress prefix. +## @retval 0 The logging decision completed successfully. +bootstrap_log_install_start() { + if bootstrap_context_is_quiet; then + return "${BOOTSTRAP_EXIT_SUCCESS}" + fi + + printf 'Installing %s...' "$1" >&2 +} + +## @fn bootstrap_log_install_done() +## @brief Completes an installation progress line successfully. +## @par Standard Error +## The text `done.` followed by a newline when quiet mode is inactive. +## @retval 0 The logging decision completed successfully. +bootstrap_log_install_done() { + if bootstrap_context_is_quiet; then + return "${BOOTSTRAP_EXIT_SUCCESS}" + fi + + printf 'done. +' >&2 +} + +## @fn bootstrap_log_install_failed() +## @brief Completes an installation progress line unsuccessfully. +## @par Standard Error +## The text `failed.` followed by a newline when quiet mode is inactive. +## @retval 0 The logging decision completed successfully. +bootstrap_log_install_failed() { + if bootstrap_context_is_quiet; then + return "${BOOTSTRAP_EXIT_SUCCESS}" + fi + + printf 'failed. +' >&2 +} diff --git a/lib/runtime/privilege.bash b/lib/runtime/privilege.bash index d16862c..6afcbd4 100644 --- a/lib/runtime/privilege.bash +++ b/lib/runtime/privilege.bash @@ -97,3 +97,35 @@ bootstrap_privilege_run() { "$@" } + + +## @fn bootstrap_privilege_run_install() +## @brief Runs one privileged package installation with an optional timeout. +## @details +## GNU timeout is used when available. Minimal systems without timeout remain +## supported, but Bootstrap emits one warning per invocation before continuing +## with an unbounded package-manager command. +## @param command Package-manager command to execute. +## @param ... Command arguments. +## @retval 0 The command completed successfully. +## @retval 124 GNU timeout ended the command after the configured duration. +## @retval 71 Required privilege escalation was unavailable. +## @retval * The package-manager command's own exit status. +bootstrap_privilege_run_install() { + local timeout + + timeout="$(bootstrap_context_get_install_timeout)" + + if command -v timeout >/dev/null 2>&1; then + bootstrap_privilege_run timeout "${timeout}" "$@" + return "$?" + fi + + if ! bootstrap_context_timeout_warning_was_emitted; then + bootstrap_log_warning \ + "timeout is unavailable; package installations will not be time-bounded" + bootstrap_context_mark_timeout_warning_emitted + fi + + bootstrap_privilege_run "$@" +} diff --git a/lib/runtime/recovery.bash b/lib/runtime/recovery.bash index 15cad98..987790a 100644 --- a/lib/runtime/recovery.bash +++ b/lib/runtime/recovery.bash @@ -220,7 +220,7 @@ bootstrap_recovery_execution_failed() { case "${manager}" in apt) - bootstrap_recovery_emit "Run the native command directly for full details: sudo apt-get install -y ${package}" + bootstrap_recovery_emit "Run the native command directly for full details: sudo apt-get install -y --no-install-recommends ${package}" bootstrap_recovery_emit 'Check for package-manager locks, network failures, or repository errors.' ;; apk) diff --git a/tests/config.bats b/tests/config.bats index 0cd9c7e..c46f7be 100644 --- a/tests/config.bats +++ b/tests/config.bats @@ -88,3 +88,43 @@ setup() { [[ "$output" == *"would install package: git"* ]] [[ "$output" != *"unsupported package manager: bogus"* ]] } + + +@test "installation timeout defaults to 30 seconds" { + run bash -c "source '$SCRIPT'; bootstrap_context_reset; bootstrap_context_get_install_timeout" + + [ "$status" -eq 0 ] + [ "$output" = "30" ] +} + +@test "environment installation timeout overrides the default" { + run env BOOTSTRAP_INSTALL_TIMEOUT=45 bash -c "source '$SCRIPT'; bootstrap_context_reset; bootstrap_config_apply_environment; bootstrap_config_validate_effective_runtime; bootstrap_context_get_install_timeout" + + [ "$status" -eq 0 ] + [ "$output" = "45" ] +} + +@test "invalid installation timeout fails conservatively" { + manifest="${WORK_DIR}/packages.txt" + printf 'git +' >"$manifest" + + run env BOOTSTRAP_INSTALL_TIMEOUT=0 "$SCRIPT" --dry-run "$manifest" + + [ "$status" -eq 64 ] + [[ "$output" == *"BOOTSTRAP_INSTALL_TIMEOUT must be a positive whole number of seconds: 0"* ]] +} + +@test "installation timeout may be loaded from .env" { + manifest="${WORK_DIR}/packages.txt" + printf 'git +' >"$manifest" + printf 'BOOTSTRAP_INSTALL_TIMEOUT=12 +BOOTSTRAP_PACKAGE_MANAGER=apt +' >"${WORK_DIR}/.env" + + run bash -c 'cd "$1" && source "$2"; bootstrap_context_reset; bootstrap_config_load_default_file; bootstrap_context_get_install_timeout' _ "$WORK_DIR" "$SCRIPT" + + [ "$status" -eq 0 ] + [ "$output" = "12" ] +} diff --git a/tests/execution-summary.bats b/tests/execution-summary.bats index d6313e7..aa95bfc 100644 --- a/tests/execution-summary.bats +++ b/tests/execution-summary.bats @@ -90,5 +90,5 @@ STUB [[ "$output" == *"installed: 1"* ]] [[ "$output" == *"failed: 0"* ]] [[ "$output" == *"not executed: 0"* ]] - [ "$(cat "$install_log")" = "install -y new-package" ] + [ "$(cat "$install_log")" = "install -y --no-install-recommends new-package" ] } diff --git a/tests/executor-apk.bats b/tests/executor-apk.bats index 39060ed..3191021 100644 --- a/tests/executor-apk.bats +++ b/tests/executor-apk.bats @@ -108,3 +108,28 @@ STUB [[ "$output" == *"apk exited with status 99"* ]] [[ "$output" == *"bootstrap.bash: recovery: Run the native command directly for full details: sudo apk add git"* ]] } + +@test "executor applies the configured timeout to apk installs" { + fake_bin="${TEST_TMPDIR}/bin" + timeout_log="${TEST_TMPDIR}/timeout.log" + mkdir -p "$fake_bin" + cat >"${fake_bin}/apk" <<'STUB' +#!/usr/bin/env bash +if [ "$1" = "info" ] && [ "$2" = "-e" ]; then + exit 1 +fi +exit 0 +STUB + cat >"${fake_bin}/timeout" <<'STUB' +#!/usr/bin/env bash +printf '%s\n' "$*" >"${TIMEOUT_LOG}" +exit 124 +STUB + chmod +x "${fake_bin}/apk" "${fake_bin}/timeout" + + run env PATH="${fake_bin}:$PATH" TIMEOUT_LOG="$timeout_log" BOOTSTRAP_INSTALL_TIMEOUT=8 bash -c "source '$SCRIPT'; bootstrap_context_reset; bootstrap_config_apply_environment; bootstrap_privilege_effective_uid() { printf '0\n'; }; printf 'install-package|apk|git||||\n' | bootstrap_executor_execute_resolved_actions" + + [ "$status" -eq 70 ] + [[ "$output" == *"package installation timed out after 8 seconds"* ]] + [ "$(cat "$timeout_log")" = "8 apk add git" ] +} diff --git a/tests/executor-dnf.bats b/tests/executor-dnf.bats index 0b63f3d..f85fdd6 100644 --- a/tests/executor-dnf.bats +++ b/tests/executor-dnf.bats @@ -113,3 +113,29 @@ STUB [[ "$output" == *"dnf exited with status 99"* ]] [[ "$output" == *"bootstrap.bash: recovery: Run the native command directly for full details: sudo dnf install -y git"* ]] } + +@test "executor applies the configured timeout to dnf installs" { + fake_bin="${TEST_TMPDIR}/bin" + timeout_log="${TEST_TMPDIR}/timeout.log" + mkdir -p "$fake_bin" + cat >"${fake_bin}/rpm" <<'STUB' +#!/usr/bin/env bash +exit 1 +STUB + cat >"${fake_bin}/dnf" <<'STUB' +#!/usr/bin/env bash +exit 0 +STUB + cat >"${fake_bin}/timeout" <<'STUB' +#!/usr/bin/env bash +printf '%s\n' "$*" >"${TIMEOUT_LOG}" +exit 124 +STUB + chmod +x "${fake_bin}/rpm" "${fake_bin}/dnf" "${fake_bin}/timeout" + + run env PATH="${fake_bin}:$PATH" TIMEOUT_LOG="$timeout_log" BOOTSTRAP_INSTALL_TIMEOUT=9 bash -c "source '$SCRIPT'; bootstrap_context_reset; bootstrap_config_apply_environment; bootstrap_privilege_effective_uid() { printf '0\n'; }; printf 'install-package|dnf|git||||\n' | bootstrap_executor_execute_resolved_actions" + + [ "$status" -eq 70 ] + [[ "$output" == *"package installation timed out after 9 seconds"* ]] + [ "$(cat "$timeout_log")" = "9 dnf install -y git" ] +} diff --git a/tests/executor-regression.bats b/tests/executor-regression.bats index e82200b..b951ce5 100644 --- a/tests/executor-regression.bats +++ b/tests/executor-regression.bats @@ -36,5 +36,5 @@ STUB [ "$status" -eq 70 ] [[ "$output" == *"failed|70|install-package|apt|first|apt-get exited with status 23"* ]] - [ "$(cat "$install_log")" = "install -y first" ] + [ "$(cat "$install_log")" = "install -y --no-install-recommends first" ] } diff --git a/tests/executor.bats b/tests/executor.bats index ea99006..5c3c3e7 100644 --- a/tests/executor.bats +++ b/tests/executor.bats @@ -57,7 +57,7 @@ STUB [ "$status" -eq 0 ] [[ "$output" == *"success|0|install-package|apt|git|package installation completed"* ]] - [ "$(cat "$log_file")" = "install -y git" ] + [ "$(cat "$log_file")" = "install -y --no-install-recommends git" ] } @test "executor preserves package identity in apt failure results" { @@ -159,5 +159,96 @@ STUB [ "$status" -eq 70 ] [[ "$output" == *"apt-get exited with status 100"* ]] - [[ "$output" == *"bootstrap.bash: recovery: Run the native command directly for full details: sudo apt-get install -y git"* ]] + [[ "$output" == *"bootstrap.bash: recovery: Run the native command directly for full details: sudo apt-get install -y --no-install-recommends git"* ]] +} + + +@test "executor reports apt installation progress outside quiet mode" { + fake_bin="${TEST_TMPDIR}/bin" + mkdir -p "$fake_bin" + cat >"${fake_bin}/dpkg-query" <<'STUB' +#!/usr/bin/env bash +exit 1 +STUB + cat >"${fake_bin}/apt-get" <<'STUB' +#!/usr/bin/env bash +exit 0 +STUB + chmod +x "${fake_bin}/dpkg-query" "${fake_bin}/apt-get" + + run env PATH="${fake_bin}:$PATH" bash -c "source '$SCRIPT'; bootstrap_privilege_effective_uid() { printf '0\n'; }; printf 'install-package|apt|git||||\n' | bootstrap_executor_execute_resolved_actions" + + [ "$status" -eq 0 ] + [[ "$output" == *"Installing git...done."* ]] +} + +@test "quiet mode suppresses apt installation progress" { + fake_bin="${TEST_TMPDIR}/bin" + mkdir -p "$fake_bin" + cat >"${fake_bin}/dpkg-query" <<'STUB' +#!/usr/bin/env bash +exit 1 +STUB + cat >"${fake_bin}/apt-get" <<'STUB' +#!/usr/bin/env bash +exit 0 +STUB + chmod +x "${fake_bin}/dpkg-query" "${fake_bin}/apt-get" + + run env PATH="${fake_bin}:$PATH" bash -c "source '$SCRIPT'; bootstrap_context_enable_quiet; bootstrap_privilege_effective_uid() { printf '0\n'; }; printf 'install-package|apt|git||||\n' | bootstrap_executor_execute_resolved_actions" + + [ "$status" -eq 0 ] + [[ "$output" != *"Installing git"* ]] +} + +@test "executor reports a timed-out apt installation" { + fake_bin="${TEST_TMPDIR}/bin" + timeout_log="${TEST_TMPDIR}/timeout.log" + mkdir -p "$fake_bin" + cat >"${fake_bin}/dpkg-query" <<'STUB' +#!/usr/bin/env bash +exit 1 +STUB + cat >"${fake_bin}/timeout" <<'STUB' +#!/usr/bin/env bash +printf '%s +' "$*" >"${TIMEOUT_LOG}" +exit 124 +STUB + cat >"${fake_bin}/apt-get" <<'STUB' +#!/usr/bin/env bash +exit 0 +STUB + chmod +x "${fake_bin}/dpkg-query" "${fake_bin}/timeout" "${fake_bin}/apt-get" + + run env PATH="${fake_bin}:$PATH" TIMEOUT_LOG="$timeout_log" BOOTSTRAP_INSTALL_TIMEOUT=7 bash -c "source '$SCRIPT'; bootstrap_context_reset; bootstrap_config_apply_environment; bootstrap_privilege_effective_uid() { printf '0\n'; }; printf 'install-package|apt|git||||\n' | bootstrap_executor_execute_resolved_actions" + + [ "$status" -eq 70 ] + [[ "$output" == *"Installing git...failed."* ]] + [[ "$output" == *"package installation timed out after 7 seconds"* ]] + [ "$(cat "$timeout_log")" = "7 apt-get install -y --no-install-recommends git" ] +} + +@test "missing timeout warns once and continues without a timeout" { + fake_bin="${TEST_TMPDIR}/bin" + apt_log="${TEST_TMPDIR}/apt.log" + mkdir -p "$fake_bin" + cat >"${fake_bin}/dpkg-query" <<'STUB' +#!/bin/sh +exit 1 +STUB + cat >"${fake_bin}/apt-get" <<'STUB' +#!/bin/sh +printf '%s +' "$*" >>"${APT_GET_LOG}" +exit 0 +STUB + chmod +x "${fake_bin}/dpkg-query" "${fake_bin}/apt-get" + + run env PATH="$fake_bin" APT_GET_LOG="$apt_log" "$BASH" -c "source '$SCRIPT'; bootstrap_context_enable_quiet; bootstrap_privilege_effective_uid() { printf '0\n'; }; printf 'install-package|apt|git||||\ninstall-package|apt|curl||||\n' | bootstrap_executor_execute_resolved_actions" + + [ "$status" -eq 0 ] + [ "$(grep -c 'timeout is unavailable' <<<"$output")" -eq 1 ] + [[ "$output" != *"Installing git"* ]] + [ "$(wc -l <"$apt_log")" -eq 2 ] } diff --git a/tests/trivial.bats b/tests/trivial.bats index 98302e7..e5920f9 100644 --- a/tests/trivial.bats +++ b/tests/trivial.bats @@ -139,7 +139,7 @@ STUB [ "$status" -eq 0 ] [[ "$output" == *"Execution results:"* ]] [[ "$output" == *"package installation completed"* ]] - [ "$(cat "$log_file")" = "install -y git" ] + [ "$(cat "$log_file")" = "install -y --no-install-recommends git" ] } @test "generated bootstrap.bash requires help to be used alone" {