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/<user>/.local/bin/install.sh"
    /usr/bin/bash: /home/<user>/.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 <dir>`
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 <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-08-15 10:43:15 +02:00
1 parent 6ecbc5f046
commit d58962c010
3 files changed
+90

No files matched your search

+30
View File
@@ -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);
+6
View File
@@ -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. */
+54
View File
@@ -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/<user>/.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 <dir>` 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);