Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/cli/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
6 changes: 6 additions & 0 deletions src/cli/cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
54 changes: 54 additions & 0 deletions tests/test_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading