From d58962c010642086d05c33d0feabd77848868328 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sat, 15 Aug 2026 10:14:01 +0200 Subject: [PATCH] fix(cli): only name an installer that is actually there `update` hands off to install.sh (install.ps1 on Windows) and prints the command to run. It built that command from cbm_detect_self_path - the BINARY's directory - and treated "I resolved my own location" as "the installer is beside me". Those are different questions. install.sh is placed beside the binary by install.sh itself, but a binary that was moved, packaged by a distro, or built from source has no installer next to it. We printed the path anyway: bash "/home//.local/bin/install.sh" /usr/bin/bash: /home//.local/bin/install.sh: No such file or directory Reported on discussion #1560 (#1632) by a user who was already three releases deep in install trouble and had just been told, by us, to run a file that does not exist. `update` exists to tell someone how to proceed. Ending the interaction on a command that cannot run is the one outcome it must not produce - and the fallback text was already there and already correct, naming install.sh as shipping in the release archive without asserting a path. The probe goes through cbm_path_info_utf8 so a non-ASCII install directory resolves on Windows, and rejects a DIRECTORY of that name, because `bash ` is not a command either. A symlink still counts: it is reported rather than followed, and the shell runs it perfectly well. The Windows branch gets the same treatment; it had the identical assumption about install.ps1. Reproduce-first and revert-checked: with the probe forced to return true - the old behaviour - the new test fails with "a directory with no installer must not be named as one", and passes once it is restored. cli: 276 passed. Signed-off-by: Martin Vogel --- src/cli/cli.c | 30 +++++++++++++++++++++++++++ src/cli/cli.h | 6 ++++++ tests/test_cli.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/src/cli/cli.c b/src/cli/cli.c index 7c0558c91..77763e5ee 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -11972,6 +11972,36 @@ static bool check_already_latest(void) { #endif /* CBM_CLI_ENABLE_TEST_API */ +/* Is the installer script actually present beside the binary? (#1632) + * + * `update` hands off to install.sh / install.ps1 and prints the command to run. + * It derived that path from the binary's own location, which is not the same + * question: the installer is placed beside the binary by install.sh, but a + * binary moved, packaged, or built from source has no installer next to it, and + * we still printed the path. The user's session then ended on a command that + * cannot run. + * + * Checked with cbm_path_info_utf8 so a non-ASCII install path resolves on + * Windows. A symlink counts: it is reported rather than followed, and the shell + * will run it perfectly well. */ +bool cbm_cli_installer_beside_binary(const char *dir) { + if (!dir || !dir[0]) { + return false; + } + char script[CLI_BUF_1K]; +#ifdef _WIN32 + const char *name = "install.ps1"; +#else + const char *name = "install.sh"; +#endif + int written = snprintf(script, sizeof(script), "%s/%s", dir, name); + if (written <= 0 || (size_t)written >= sizeof(script)) { + return false; + } + cbm_path_info_t info; + return cbm_path_info_utf8(script, &info) == 0 && !info.is_directory; +} + int cbm_cmd_update(int argc, char **argv) { parse_auto_answer(argc, argv); diff --git a/src/cli/cli.h b/src/cli/cli.h index 184c295af..c2121a940 100644 --- a/src/cli/cli.h +++ b/src/cli/cli.h @@ -482,6 +482,12 @@ int cbm_cmd_uninstall(int argc, char **argv); /* update: check latest release, prompt for index deletion, prompt for ui/standard, * download and replace binary. */ +/* True when the installer script (install.sh / install.ps1 on Windows) is + * present beside the binary. `update` prints a command built from this; + * naming a path that does not exist ends the interaction on a failing + * command (#1632). Exposed for testing. */ +bool cbm_cli_installer_beside_binary(const char *dir); + int cbm_cmd_update(int argc, char **argv); /* config: get/set/list/reset runtime config values. */ diff --git a/tests/test_cli.c b/tests/test_cli.c index 2118b794f..f7926503a 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -12636,7 +12636,61 @@ TEST(cli_windows_update_hands_off_to_install_script) { * Suite definition * ═══════════════════════════════════════════════════════════════════ */ +/* #1632: `update` derives the installer command from the BINARY's directory, + * which is not the same question as "is the installer there". A binary in + * ~/.local/bin with no install.sh beside it still produced: + * + * bash "/home//.local/bin/install.sh" + * /usr/bin/bash: .../install.sh: No such file or directory + * + * reported on discussion #1560 by a user already three releases deep in install + * trouble. `update` exists to tell someone how to proceed; ending on a command + * that cannot run is the one outcome it must not produce. */ +TEST(cli_update_only_names_an_installer_that_exists_issue1632) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-installer-probe-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) { + FAIL("cbm_mkdtemp failed"); + } +#ifdef _WIN32 + const char *installer = "install.ps1"; +#else + const char *installer = "install.sh"; +#endif + + /* A directory holding the binary but no installer must not be advertised. */ + bool absent_is_refused = !cbm_cli_installer_beside_binary(tmpdir); + + char script[512]; + snprintf(script, sizeof(script), "%s/%s", tmpdir, installer); + write_test_file(script, "#!/bin/sh\nexit 0\n"); + bool present_is_accepted = cbm_cli_installer_beside_binary(tmpdir); + + /* A directory of that name must not count: `bash ` is not a command. */ + char decoy[512]; + snprintf(decoy, sizeof(decoy), "%s/decoy", tmpdir); + test_mkdirp(decoy); + char decoy_installer[640]; + snprintf(decoy_installer, sizeof(decoy_installer), "%s/%s", decoy, installer); + test_mkdirp(decoy_installer); + bool directory_is_refused = !cbm_cli_installer_beside_binary(decoy); + + bool empty_is_refused = !cbm_cli_installer_beside_binary(""); + test_rmdir_r(tmpdir); + + if (!absent_is_refused) + FAIL("a directory with no installer must not be named as one"); + if (!present_is_accepted) + FAIL("an installer that is present must be named"); + if (!directory_is_refused) + FAIL("a DIRECTORY named install.sh is not a runnable installer"); + if (!empty_is_refused) + FAIL("an empty directory string must be refused"); + PASS(); +} + SUITE(cli) { + RUN_TEST(cli_update_only_names_an_installer_that_exists_issue1632); RUN_TEST(cli_progress_visibility_policy); RUN_TEST(cli_raw_mcp_result_preserves_tool_error_status); RUN_TEST(cli_maintenance_cancellation_forces_failure_status);