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);