From 769e4a7a9f7a07165c1715ba937f9c6912c6fcad Mon Sep 17 00:00:00 2001 From: Dimitris Panokostas Date: Sat, 13 Jun 2026 21:15:16 +0200 Subject: [PATCH 1/2] feat: add host-env tool --- Makefile | 9 +- README.md | 18 ++- host-env | Bin 0 -> 15732 bytes package/Help/Host-Tools.guide | 23 +++- package/Install | 1 + src/host-env.c | 144 +++++++++++++++++++++ src/host_env_command.h | 198 +++++++++++++++++++++++++++++ tests/test_host_command_builders.c | 115 +++++++++++++++++ tests/test_package_layout.sh | 6 +- 9 files changed, 506 insertions(+), 8 deletions(-) create mode 100755 host-env create mode 100644 src/host-env.c create mode 100644 src/host_env_command.h diff --git a/Makefile b/Makefile index 8dcac1c..8dafc23 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ # SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas # SPDX-License-Identifier: GPL-3.0-or-later -TOOLS = host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download +TOOLS = host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download host-env TEST_BINS = tests/test_host_common.out tests/test_host_command_builders.out tests/test_host_edit_command.out tests/test_host_download_command.out TEST_SCRIPTS = tests/test_package_layout.sh tests/test_ahi_driver_source.sh TESTS = $(TEST_BINS) $(TEST_SCRIPTS) -COMMON_HEADERS = src/host_common.h src/host_path.h src/host_capture.h src/host_base64.h src/host_clip_command.h src/host_download_command.h src/host_edit_command.h src/host_info_command.h src/host_notify_command.h src/host_powershell.h src/host_reveal_command.h src/uae_pragmas.h +COMMON_HEADERS = src/host_common.h src/host_path.h src/host_capture.h src/host_base64.h src/host_clip_command.h src/host_download_command.h src/host_edit_command.h src/host_env_command.h src/host_info_command.h src/host_notify_command.h src/host_powershell.h src/host_reveal_command.h src/uae_pragmas.h PACKAGE = Host-Tools-$(VERSION).lha PACKAGE_ROOT = Host-Tools PACKAGE_DIR ?= build/package @@ -91,10 +91,13 @@ host-info: src/host-info.c $(COMMON_HEADERS) host-download: src/host-download.c $(COMMON_HEADERS) $(CC) $(CFLAGS) $(VERFLAGS) $(INCLUDES) src/host-download.c -o $@ +host-env: src/host-env.c $(COMMON_HEADERS) + $(CC) $(CFLAGS) $(VERFLAGS) $(INCLUDES) src/host-env.c -o $@ + tests/test_host_common.out: tests/test_host_common.c src/host_common.h $(HOST_CC) $(HOST_NATIVE_FLAGS) $(HOST_CFLAGS) tests/test_host_common.c -o $@ -tests/test_host_command_builders.out: tests/test_host_command_builders.c src/host_base64.h src/host_clip_command.h src/host_common.h src/host_info_command.h src/host_notify_command.h src/host_powershell.h src/host_reveal_command.h +tests/test_host_command_builders.out: tests/test_host_command_builders.c src/host_base64.h src/host_clip_command.h src/host_common.h src/host_env_command.h src/host_info_command.h src/host_notify_command.h src/host_powershell.h src/host_reveal_command.h $(HOST_CC) $(HOST_NATIVE_FLAGS) $(HOST_CFLAGS) tests/test_host_command_builders.c -o $@ tests/test_host_edit_command.out: tests/test_host_edit_command.c src/host_edit_command.h src/host_common.h diff --git a/README.md b/README.md index 8a0b909..9350709 100644 --- a/README.md +++ b/README.md @@ -115,13 +115,27 @@ host-info host-download [] [FORCE] ``` +### 11. host-env +**Get and set host user environment variables.** +`host-env` reads, writes, removes, and lists environment variables on the host. On Windows it updates the user's persistent environment. On Linux and macOS it writes persistent values to `$HOME/.host-tools-env`; source that file from your host shell startup files if you want future host login shells to import those values automatically. +- **Persistent**: Changes are intended for future host processes. +- **Scoped Safely**: Existing host shells, desktop apps, and Amiberry's parent process cannot have their live environment changed by a child process. + +**Usage:** +```shell +host-env get NAME +host-env set NAME VALUE +host-env unset NAME +host-env list +``` + --- ## Requirements - **Amiberry v6.0+** (or a version with updated `uaelib` support). - "Native Code" execution must be enabled in Amiberry settings. -- All tools work on **Linux and macOS hosts**. On **Windows hosts**, `host-path`, `host-download`, `host-clip`, `host-reveal`, and `host-info` are supported with a current Amiberry (PowerShell and Explorer handle the Windows side; `curl.exe` ships with Windows 10 and later). The remaining tools (`host-run`, `host-multiview`, `host-shell`, `host-edit`, `host-notify`) currently require a Linux or macOS host, since their host commands run through the POSIX shell. +- All tools work on **Linux and macOS hosts**. On **Windows hosts**, `host-path`, `host-download`, `host-clip`, `host-reveal`, `host-info`, and `host-env` are supported with a current Amiberry (PowerShell and Explorer handle the Windows side; `curl.exe` ships with Windows 10 and later). The remaining tools (`host-run`, `host-multiview`, `host-shell`, `host-edit`, `host-notify`) currently require a Linux or macOS host, since their host commands run through the POSIX shell. - For status-aware tools (`host-reveal`, `host-notify`, `host-clip`, and `host-info`), a newer Amiberry build with the `HostShell_Status` trap reports host command failures immediately. Older builds still work, but use timeout-based completion detection. - Linux desktop integration uses `xdg-utils` (`xdg-open`, `xdg-mime`) and GTK's `gtk-launch` when available. Notifications use `notify-send`; clipboard support uses `wl-clipboard`, `xclip`, or `xsel`; file selection in `host-reveal` uses `gdbus` when present. Character set conversion uses `iconv` when present. - `host-download` uses the host's `curl` or `wget` (`curl.exe` on Windows). Live streaming progress requires an Amiberry build with pipe-based HostShell sessions; on older Linux and macOS builds the tool falls back to a two-phase transfer that downloads on the host first. @@ -138,7 +152,7 @@ All tools follow AmigaDOS conventions: `0` on success, `10` (`RETURN_ERROR`) whe The installer shows a component checklist for the command tools, AmigaGuide documentation, and the UAE and UAESND AHI audio drivers. Before replacing an existing versioned file, it shows the installed and package versions and asks whether to replace or skip it. Files without version strings are still checked for existence and ask before overwrite. It does not edit startup files, system settings, AHI preferences, or existing driver configuration. -For manual installation, copy the binaries (`host-run`, `host-multiview`, `host-shell`, `host-path`, `host-reveal`, `host-notify`, `host-edit`, `host-clip`, `host-info`) from the package `C` drawer to `C:` or anywhere in your system path. To install the UAE AHI driver manually, copy `Devs/AHI/uae.audio` to `DEVS:AHI/` and `Devs/AudioModes/UAE` to `DEVS:AudioModes/`. To install the UAESND AHI driver manually, copy `Devs/AHI/uaesnd.audio` to `DEVS:AHI/` and `Devs/AudioModes/UAESND` to `DEVS:AudioModes/`. The UAESND driver plays each AHI channel through a hardware audio stream and requires the UAESND sound board to be enabled in the Amiberry configuration. +For manual installation, copy the binaries (`host-run`, `host-multiview`, `host-shell`, `host-path`, `host-reveal`, `host-notify`, `host-edit`, `host-clip`, `host-info`, `host-download`, `host-env`) from the package `C` drawer to `C:` or anywhere in your system path. To install the UAE AHI driver manually, copy `Devs/AHI/uae.audio` to `DEVS:AHI/` and `Devs/AudioModes/UAE` to `DEVS:AudioModes/`. To install the UAESND AHI driver manually, copy `Devs/AHI/uaesnd.audio` to `DEVS:AHI/` and `Devs/AudioModes/UAESND` to `DEVS:AudioModes/`. The UAESND driver plays each AHI channel through a hardware audio stream and requires the UAESND sound board to be enabled in the Amiberry configuration. ## Examples diff --git a/host-env b/host-env new file mode 100755 index 0000000000000000000000000000000000000000..4cbf0db89d79d76e7cc9438ef8053c91a593a8ca GIT binary patch literal 15732 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 +#include +#include "host_capture.h" +#include "host_env_command.h" + +static const char version[] = "$VER: Host-Env " VERSION_STR " (" DATE_STR ")"; + +static int print_usage(void) +{ + printf("Host-Env v%s\n", VERSION_STR); + printf("Host-Env gets and sets host user environment variables.\n"); + printf("%s\nUsage: host-env get \n" + " host-env set \n" + " host-env unset \n" + " host-env list\n", version); + return 0; +} + +static int require_name(const char *name) +{ + if (!host_env_valid_name(name)) { + printf("Invalid environment variable name\n"); + return 0; + } + return 1; +} + +int main(int argc, char *argv[]) +{ + static char command[HOST_MAX_COMMAND_LEN]; + int windows; + + command[0] = '\0'; + + if (!InitUAEResource()) + { + printf("UAEResource not found!\n"); + return 2; + } + + if (argc <= 1) + { + printf("Missing environment command\n"); + print_usage(); + return HOST_RETURN_ERROR; + } + + if (strcmp(argv[1], "?") == 0) + { + return print_usage(); + } + + windows = (GetHostPlatform() == HOST_PLATFORM_WINDOWS); + + if (strcmp(argv[1], "get") == 0) { + if (argc != 3) { + printf("Usage error\n"); + print_usage(); + return HOST_RETURN_ERROR; + } + if (!require_name(argv[2])) { + return HOST_RETURN_ERROR; + } + if (windows) { + if (!host_append_env_get_command_windows(command, sizeof(command), argv[2])) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + } else if (!host_append_env_get_command(command, sizeof(command), argv[2])) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + return host_print_command_output(command); + } + + if (strcmp(argv[1], "set") == 0) { + if (argc != 4) { + printf("Usage error\n"); + print_usage(); + return HOST_RETURN_ERROR; + } + if (!require_name(argv[2])) { + return HOST_RETURN_ERROR; + } + if (windows) { + if (!host_append_env_set_command_windows(command, sizeof(command), argv[2], argv[3])) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + } else if (!host_append_env_set_command(command, sizeof(command), argv[2], argv[3])) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + return host_print_command_output(command); + } + + if (strcmp(argv[1], "unset") == 0) { + if (argc != 3) { + printf("Usage error\n"); + print_usage(); + return HOST_RETURN_ERROR; + } + if (!require_name(argv[2])) { + return HOST_RETURN_ERROR; + } + if (windows) { + if (!host_append_env_unset_command_windows(command, sizeof(command), argv[2])) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + } else if (!host_append_env_unset_command(command, sizeof(command), argv[2])) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + return host_print_command_output(command); + } + + if (strcmp(argv[1], "list") == 0) { + if (argc != 2) { + printf("Unexpected argument after list\n"); + print_usage(); + return HOST_RETURN_ERROR; + } + if (windows) { + if (!host_append_env_list_command_windows(command, sizeof(command))) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + } else if (!host_append_env_list_command(command, sizeof(command))) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + return host_print_command_output(command); + } + + printf("Unknown environment command\n"); + print_usage(); + return HOST_RETURN_ERROR; +} diff --git a/src/host_env_command.h b/src/host_env_command.h new file mode 100644 index 0000000..7659e7a --- /dev/null +++ b/src/host_env_command.h @@ -0,0 +1,198 @@ +/* + * SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef HOST_ENV_COMMAND_H +#define HOST_ENV_COMMAND_H + +#include +#include "host_common.h" +#include "host_powershell.h" + +static inline int host_env_is_alpha_or_underscore(char c) +{ + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; +} + +static inline int host_env_is_alnum_or_underscore(char c) +{ + return host_env_is_alpha_or_underscore(c) || (c >= '0' && c <= '9'); +} + +static inline int host_env_valid_name(const char *name) +{ + if (name == NULL || !host_env_is_alpha_or_underscore(name[0])) { + return 0; + } + + for (const char *p = name + 1; *p; p++) { + if (!host_env_is_alnum_or_underscore(*p)) { + return 0; + } + } + return 1; +} + +static inline int host_append_env_export_line(char *line, size_t line_size, + const char *name, const char *value) +{ + if (!host_append_literal(line, line_size, "export ") || + !host_append_literal(line, line_size, name) || + !host_append_literal(line, line_size, "='")) { + return 0; + } + + for (const char *p = value; *p; p++) { + char one[2]; + + if (*p == '\'') { + if (!host_append_literal(line, line_size, "'\\''")) { + return 0; + } + } else { + one[0] = *p; + one[1] = '\0'; + if (!host_append_literal(line, line_size, one)) { + return 0; + } + } + } + + return host_append_literal(line, line_size, "'"); +} + +static inline int host_append_env_file_prefix(char *command, size_t command_size) +{ + return host_append_literal(command, command_size, + "f=\"${HOME:?}/.host-tools-env\"; "); +} + +static inline int host_append_env_get_command(char *command, size_t command_size, + const char *name) +{ + if (!host_env_valid_name(name)) { + return 0; + } + + return host_append_env_file_prefix(command, command_size) && + host_append_literal(command, command_size, "if [ \"${") && + host_append_literal(command, command_size, name) && + host_append_literal(command, command_size, "+x}\" = x ]; then printf %s \"$") && + host_append_literal(command, command_size, name) && + host_append_literal(command, command_size, "\"; elif [ -r \"$f\" ]; then . \"$f\"; if [ \"${") && + host_append_literal(command, command_size, name) && + host_append_literal(command, command_size, "+x}\" = x ]; then printf %s \"$") && + host_append_literal(command, command_size, name) && + host_append_literal(command, command_size, "\"; else exit 1; fi; else exit 1; fi"); +} + +static inline int host_append_env_set_command(char *command, size_t command_size, + const char *name, const char *value) +{ + static char line[HOST_MAX_COMMAND_LEN]; + + if (!host_env_valid_name(name)) { + return 0; + } + + line[0] = '\0'; + if (!host_append_env_export_line(line, sizeof(line), name, value)) { + return 0; + } + + return host_append_env_file_prefix(command, command_size) && + host_append_literal(command, command_size, + "t=\"${f}.$$\"; touch \"$f\" || exit $?; grep -v '^export ") && + host_append_literal(command, command_size, name) && + host_append_literal(command, command_size, "=' \"$f\" > \"$t\" 2>/dev/null || true; printf '%s\\n' ") && + host_append_shell_arg(command, command_size, line, 0) && + host_append_literal(command, command_size, " >> \"$t\" && mv \"$t\" \"$f\""); +} + +static inline int host_append_env_unset_command(char *command, size_t command_size, + const char *name) +{ + if (!host_env_valid_name(name)) { + return 0; + } + + return host_append_env_file_prefix(command, command_size) && + host_append_literal(command, command_size, + "t=\"${f}.$$\"; if [ -f \"$f\" ]; then grep -v '^export ") && + host_append_literal(command, command_size, name) && + host_append_literal(command, command_size, "=' \"$f\" > \"$t\" || true; mv \"$t\" \"$f\"; fi"); +} + +static inline int host_append_env_list_command(char *command, size_t command_size) +{ + return host_append_env_file_prefix(command, command_size) && + host_append_literal(command, command_size, + "if [ -r \"$f\" ]; then . \"$f\"; fi; env | sort"); +} + +static inline int host_append_env_get_command_windows(char *command, size_t command_size, + const char *name) +{ + static char script[HOST_MAX_COMMAND_LEN]; + + if (!host_env_valid_name(name)) { + return 0; + } + + script[0] = '\0'; + return host_append_literal(script, sizeof(script), + "[Console]::OutputEncoding=[System.Text.Encoding]::GetEncoding(28591);" + "$v=[Environment]::GetEnvironmentVariable(") && + host_append_ps_quoted(script, sizeof(script), name) && + host_append_literal(script, sizeof(script), ",'User');" + "if($null -eq $v){exit 1};[Console]::Out.Write($v)") && + host_append_ps_encoded_command(command, command_size, script); +} + +static inline int host_append_env_set_command_windows(char *command, size_t command_size, + const char *name, + const char *value) +{ + static char script[HOST_MAX_COMMAND_LEN]; + + if (!host_env_valid_name(name)) { + return 0; + } + + script[0] = '\0'; + return host_append_literal(script, sizeof(script), + "[Environment]::SetEnvironmentVariable(") && + host_append_ps_quoted(script, sizeof(script), name) && + host_append_literal(script, sizeof(script), ",") && + host_append_ps_quoted(script, sizeof(script), value) && + host_append_literal(script, sizeof(script), ",'User')") && + host_append_ps_encoded_command(command, command_size, script); +} + +static inline int host_append_env_unset_command_windows(char *command, size_t command_size, + const char *name) +{ + static char script[HOST_MAX_COMMAND_LEN]; + + if (!host_env_valid_name(name)) { + return 0; + } + + script[0] = '\0'; + return host_append_literal(script, sizeof(script), + "[Environment]::SetEnvironmentVariable(") && + host_append_ps_quoted(script, sizeof(script), name) && + host_append_literal(script, sizeof(script), ",$null,'User')") && + host_append_ps_encoded_command(command, command_size, script); +} + +static inline int host_append_env_list_command_windows(char *command, size_t command_size) +{ + return host_append_ps_encoded_command(command, command_size, + "[Console]::OutputEncoding=[System.Text.Encoding]::GetEncoding(28591);" + "[Environment]::GetEnvironmentVariables('User').GetEnumerator()|" + "Sort-Object Name|ForEach-Object{[Console]::Out.WriteLine(($_.Name)+'='+($_.Value))}"); +} + +#endif diff --git a/tests/test_host_command_builders.c b/tests/test_host_command_builders.c index 8fa0ab1..56eaaa2 100644 --- a/tests/test_host_command_builders.c +++ b/tests/test_host_command_builders.c @@ -8,6 +8,7 @@ #include #include "host_base64.h" #include "host_clip_command.h" +#include "host_env_command.h" #include "host_info_command.h" #include "host_notify_command.h" #include "host_powershell.h" @@ -142,6 +143,76 @@ static void test_info_command(void) require_shell_syntax(HOST_INFO_COMMAND); } +static void test_env_name_validation(void) +{ + require(host_env_valid_name("FOO"), "simple env name should be valid"); + require(host_env_valid_name("_FOO_1"), "underscore env name should be valid"); + require(!host_env_valid_name(""), "empty env name should be invalid"); + require(!host_env_valid_name("1FOO"), "env name must not start with a digit"); + require(!host_env_valid_name("BAD-NAME"), "env name must not contain hyphens"); + require(!host_env_valid_name("BAD=NAME"), "env name must not contain equals"); +} + +static void test_env_get_command(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_env_get_command(command, sizeof(command), "FOO"), + "env get command should build"); + + require_contains(command, "f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "if [ \"${FOO+x}\" = x ]; then printf %s \"$FOO\""); + require_contains(command, ". \"$f\""); + require_contains(command, "else exit 1; fi"); + require_shell_syntax(command); +} + +static void test_env_set_command(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_env_set_command(command, sizeof(command), + "FOO", "a b's $HOME"), + "env set command should build"); + + require_contains(command, "f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "grep -v '^export FOO=' \"$f\""); + require_contains(command, "printf '%s\\n' 'export FOO='\\''a b'\\''\\'\\'''\\''s $HOME'\\'''"); + require_contains(command, "mv \"$t\" \"$f\""); + require_shell_syntax(command); +} + +static void test_env_unset_command(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_env_unset_command(command, sizeof(command), "FOO"), + "env unset command should build"); + + require_contains(command, "f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "grep -v '^export FOO=' \"$f\" > \"$t\""); + require_contains(command, "mv \"$t\" \"$f\""); + require_shell_syntax(command); +} + +static void test_env_list_command(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_env_list_command(command, sizeof(command)), + "env list command should build"); + + require_contains(command, "f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "env"); + require_contains(command, ". \"$f\""); + require_contains(command, "sort"); + require_shell_syntax(command); +} + static void require_ps_script(const char *command, const char *script) { static const char prefix[] = "powershell -NoProfile -EncodedCommand "; @@ -215,6 +286,40 @@ static void test_windows_info_command(void) require_ps_script(command, HOST_INFO_PS_SCRIPT); } +static void test_windows_env_commands(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_env_get_command_windows(command, sizeof(command), "FOO"), + "windows env get command should build"); + require_ps_script(command, + "[Console]::OutputEncoding=[System.Text.Encoding]::GetEncoding(28591);" + "$v=[Environment]::GetEnvironmentVariable('FOO','User');" + "if($null -eq $v){exit 1};[Console]::Out.Write($v)"); + + command[0] = '\0'; + require(host_append_env_set_command_windows(command, sizeof(command), + "FOO", "a'b"), + "windows env set command should build"); + require_ps_script(command, + "[Environment]::SetEnvironmentVariable('FOO','a''b','User')"); + + command[0] = '\0'; + require(host_append_env_unset_command_windows(command, sizeof(command), "FOO"), + "windows env unset command should build"); + require_ps_script(command, + "[Environment]::SetEnvironmentVariable('FOO',$null,'User')"); + + command[0] = '\0'; + require(host_append_env_list_command_windows(command, sizeof(command)), + "windows env list command should build"); + require_ps_script(command, + "[Console]::OutputEncoding=[System.Text.Encoding]::GetEncoding(28591);" + "[Environment]::GetEnvironmentVariables('User').GetEnumerator()|" + "Sort-Object Name|ForEach-Object{[Console]::Out.WriteLine(($_.Name)+'='+($_.Value))}"); +} + static void test_small_buffer_failures(void) { char command[32]; @@ -230,6 +335,10 @@ static void test_small_buffer_failures(void) command[0] = '\0'; require(!host_append_clip_copy_command(command, sizeof(command), "Text"), "small clipboard copy command buffer should fail"); + + command[0] = '\0'; + require(!host_append_env_set_command(command, sizeof(command), "FOO", "Text"), + "small env set command buffer should fail"); } int main(void) @@ -241,10 +350,16 @@ int main(void) test_clip_copy_command_multiline(); test_clip_paste_command(); test_info_command(); + test_env_name_validation(); + test_env_get_command(); + test_env_set_command(); + test_env_unset_command(); + test_env_list_command(); test_ps_quoting(); test_windows_clip_commands(); test_windows_reveal_command(); test_windows_info_command(); + test_windows_env_commands(); test_small_buffer_failures(); return 0; } diff --git a/tests/test_package_layout.sh b/tests/test_package_layout.sh index cae1d63..0581bc4 100644 --- a/tests/test_package_layout.sh +++ b/tests/test_package_layout.sh @@ -102,7 +102,7 @@ if [ "$(drawer_window_hex "$PACKAGE_ROOT/Help.info")" != "0050002801900096" ]; t exit 1 fi -for tool in host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download; do +for tool in host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download host-env; do test -f "$PACKAGE_ROOT/C/$tool" done grep -a -q '\$VER: Host-Run 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-run" @@ -115,6 +115,7 @@ grep -a -q '\$VER: Host-Edit 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-edit" grep -a -q '\$VER: Host-Clip 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-clip" grep -a -q '\$VER: Host-Info 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-info" grep -a -q '\$VER: Host-Download 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-download" +grep -a -q '\$VER: Host-Env 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-env" if grep -a '\$VER: Host-[A-Za-z]* v[0-9]' "$PACKAGE_ROOT/C/"*; then echo "Host command \$VER strings must be parseable by Installer getversion, without a v prefix before the version number" >&2 exit 1 @@ -136,6 +137,7 @@ grep -q '^@database' "$PACKAGE_ROOT/Help/Host-Tools.guide" grep -q 'host-run' "$PACKAGE_ROOT/Help/Host-Tools.guide" grep -q 'host-shell' "$PACKAGE_ROOT/Help/Host-Tools.guide" grep -q 'host-clip' "$PACKAGE_ROOT/Help/Host-Tools.guide" +grep -q 'host-env' "$PACKAGE_ROOT/Help/Host-Tools.guide" grep -q 'UAE :16 bit HIFI Stereo++' "$PACKAGE_ROOT/Help/Host-Tools.guide" grep -q 'uaesnd: Stereo' "$PACKAGE_ROOT/Help/Host-Tools.guide" grep -q 'uaesnd: HiFi Stereo' "$PACKAGE_ROOT/Help/Host-Tools.guide" @@ -217,7 +219,7 @@ if ! grep -q "^(exit)" "$PACKAGE_ROOT/Install"; then exit 1 fi -for tool in host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download; do +for tool in host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download host-env; do grep -q "(P_InstallCommandTool \"$tool\")" "$PACKAGE_ROOT/Install" done From f18f3f6219734aee5a967693d6273637096467d5 Mon Sep 17 00:00:00 2001 From: Dimitris Panokostas Date: Sat, 13 Jun 2026 21:23:44 +0200 Subject: [PATCH 2/2] fix: ignore generated host-env binary --- .gitignore | 1 + host-env | Bin 15732 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100755 host-env diff --git a/.gitignore b/.gitignore index 0c31bc1..72dba90 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,4 @@ host-edit host-clip host-info host-download +host-env diff --git a/host-env b/host-env deleted file mode 100755 index 4cbf0db89d79d76e7cc9438ef8053c91a593a8ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15732 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