From 8793de024ca9e7feac827ee7d1b380dbdddab6e1 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Mon, 23 Mar 2026 23:32:34 +0000 Subject: [PATCH 1/5] Derive app name from binary path, allow long name override via config Resolve os.Args[0] through symlinks to get the real binary name at runtime. Symlinks behave as aliases (same identity), while copies or hard links produce a distinct app name and config directory. The display long name can be overridden via the app_long_name config key, falling back to the compiled-in default when unset. --- cmd/root.go | 38 +++++---- internal/config/settings.go | 4 + main.go | 29 ++++++- main_test.go | 64 +++++++++++++++ test/integration/test-runtime-name.sh | 108 ++++++++++++++++++++++++++ 5 files changed, 225 insertions(+), 18 deletions(-) create mode 100644 main_test.go create mode 100755 test/integration/test-runtime-name.sh diff --git a/cmd/root.go b/cmd/root.go index d9b6c9c..14f8fd0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -48,9 +48,28 @@ var ( rootCtxt = rootContext{} ) -func InitCommands(appName string, appLongName string, version string, buildNum string) { +func InitCommands(appName string, defaultLongName string, version string, buildNum string) { + // Initialize context and load config first so we can read settings + log.SetLevel(log.FatalLevel) + rootCtxt.appCtx = ctx.InitContext(appName, version, buildNum) + config.LoadConfig(rootCtxt.appCtx) + config.InitLog(rootCtxt.appCtx.AppName()) + + // Resolve the long name: prefer config, fall back to compiled-in default + appLongName := viper.GetString(config.APP_LONG_NAME_KEY) + if appLongName == "" { + appLongName = defaultLongName + } + + // Create root command with resolved names rootCmd = createRootCmd(appName, appLongName) - initApp(appName, version, buildNum) + + // Initialize remaining components + rootCtxt.cmdUpdaters = make([]*updater.CmdUpdater, 0) + initUser() + initBackend() + addBuiltinCommands() + initFrontend() } func createRootCmd(appName string, appLongName string) *cobra.Command { @@ -76,21 +95,6 @@ Example: } } -func initApp(appName string, appVersion string, buildNum string) { - log.SetLevel(log.FatalLevel) - rootCtxt.appCtx = ctx.InitContext(appName, appVersion, buildNum) - config.LoadConfig(rootCtxt.appCtx) - config.InitLog(rootCtxt.appCtx.AppName()) - - rootCtxt.cmdUpdaters = make([]*updater.CmdUpdater, 0) - - initUser() - initBackend() - - addBuiltinCommands() - initFrontend() -} - // We have to add the ctrl+C func Execute() { if err := rootCmd.Execute(); err != nil { diff --git a/internal/config/settings.go b/internal/config/settings.go index 3bb515c..3055a6e 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -39,6 +39,7 @@ const ( ENABLE_PACKAGE_SETUP_HOOK_KEY = "ENABLE_PACKAGE_SETUP_HOOK" GROUP_HELP_BY_REGISTRY_KEY = "GROUP_HELP_BY_REGISTRY" ENABLE_WORKSPACE_PACKAGES_KEY = "ENABLE_WORKSPACE_PACKAGES" + APP_LONG_NAME_KEY = "APP_LONG_NAME" // internal commands are the commands with start partition number > INTERNAL_START_PARTITION INTERNAL_COMMAND_ENABLED_KEY = "INTERNAL_COMMAND_ENABLED" @@ -81,6 +82,7 @@ func init() { ENABLE_PACKAGE_SETUP_HOOK_KEY, GROUP_HELP_BY_REGISTRY_KEY, ENABLE_WORKSPACE_PACKAGES_KEY, + APP_LONG_NAME_KEY, ) } @@ -141,6 +143,8 @@ func SetSettingValue(key string, value string) error { return setBooleanConfig(upperKey, value) case ENABLE_WORKSPACE_PACKAGES_KEY: return setBooleanConfig(upperKey, value) + case APP_LONG_NAME_KEY: + return setStringConfig(upperKey, value) } return fmt.Errorf("unsupported config %s", key) diff --git a/main.go b/main.go index 862b4d9..0f52b6f 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,10 @@ package main import ( + "os" + "path/filepath" + "strings" + root "github.com/jdevera/command-launcher/cmd" ) @@ -11,7 +15,30 @@ var buildNum string = "local" var appName string = "cdt" var appLongName string = "Criteo Dev Toolkit" +// resolveAppName derives the application name from the real path of the +// running binary. Symlinks are resolved so that symbolic links behave as +// aliases (same binary identity), while copies or hard links produce a +// distinct name and therefore a separate configuration directory. +// Falls back to the compiled-in appName on any error. +func resolveAppName() string { + exe, err := os.Executable() + if err != nil { + return appName + } + resolved, err := filepath.EvalSymlinks(exe) + if err != nil { + resolved = exe + } + name := filepath.Base(resolved) + name = strings.TrimSuffix(name, filepath.Ext(name)) + if name == "" || name == "." { + return appName + } + return name +} + func main() { - root.InitCommands(appName, appLongName, version, buildNum) + runtimeAppName := resolveAppName() + root.InitCommands(runtimeAppName, appLongName, version, buildNum) root.Execute() } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..ac738d7 --- /dev/null +++ b/main_test.go @@ -0,0 +1,64 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolveAppName_FromBinaryName(t *testing.T) { + // The test binary itself is the running executable, so resolveAppName + // should return its base name (without extension) rather than the + // compiled-in default. + name := resolveAppName() + assert.NotEmpty(t, name) + assert.NotEqual(t, ".", name) +} + +func TestResolveAppName_SymlinkResolvesToOriginal(t *testing.T) { + tmpDir := t.TempDir() + + // Create a dummy executable + original := filepath.Join(tmpDir, "original-app") + err := os.WriteFile(original, []byte("binary"), 0755) + assert.NoError(t, err) + + // Create a symlink to it + link := filepath.Join(tmpDir, "my-alias") + err = os.Symlink(original, link) + assert.NoError(t, err) + + // Resolve the symlink — should get the original name + resolved, err := filepath.EvalSymlinks(link) + assert.NoError(t, err) + assert.Equal(t, "original-app", filepath.Base(resolved)) +} + +func TestResolveAppName_CopyGetsOwnName(t *testing.T) { + tmpDir := t.TempDir() + + // Create two separate files (simulating a copy) + original := filepath.Join(tmpDir, "original-app") + err := os.WriteFile(original, []byte("binary"), 0755) + assert.NoError(t, err) + + copied := filepath.Join(tmpDir, "my-copy") + err = os.WriteFile(copied, []byte("binary"), 0755) + assert.NoError(t, err) + + // Each resolves to its own name + resolvedOrig, err := filepath.EvalSymlinks(original) + assert.NoError(t, err) + assert.Equal(t, "original-app", filepath.Base(resolvedOrig)) + + resolvedCopy, err := filepath.EvalSymlinks(copied) + assert.NoError(t, err) + assert.Equal(t, "my-copy", filepath.Base(resolvedCopy)) +} + +func TestResolveAppName_ExtensionStripped(t *testing.T) { + name := "myapp.exe" + assert.Equal(t, "myapp", name[:len(name)-len(filepath.Ext(name))]) +} diff --git a/test/integration/test-runtime-name.sh b/test/integration/test-runtime-name.sh new file mode 100755 index 0000000..d6d96b2 --- /dev/null +++ b/test/integration/test-runtime-name.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +# required environment varibale +# CL_PATH +# CL_HOME +# OUTPUT_DIR +SCRIPT_DIR=${1:-$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )} + +## +# test binary name becomes app name +## +echo "> test binary name is used as app name" +RESULT=$($CL_PATH version) +echo "$RESULT" | grep -q "^cl version" +if [ $? -ne 0 ]; then + echo "KO - expected app name 'cl' from binary name" + exit 1 +else + echo "OK" +fi + +## +# test copied binary gets its own name +## +echo "> test copied binary gets its own name" +COPIED=$OUTPUT_DIR/myapp +cp $CL_PATH $COPIED +export MYAPP_HOME=$OUTPUT_DIR/myapp-home +mkdir -p $MYAPP_HOME + +RESULT=$($COPIED version) +echo "$RESULT" | grep -q "^myapp version" +if [ $? -ne 0 ]; then + echo "KO - expected app name 'myapp' from copied binary" + rm -f $COPIED + rm -rf $MYAPP_HOME + exit 1 +else + echo "OK" +fi + +## +# test symlink resolves to original name +## +echo "> test symlink resolves to original binary name" +LINK=$OUTPUT_DIR/myalias +ln -sf $CL_PATH $LINK + +RESULT=$($LINK version) +echo "$RESULT" | grep -q "^cl version" +if [ $? -ne 0 ]; then + echo "KO - symlink should resolve to original name 'cl'" + rm -f $LINK $COPIED + rm -rf $MYAPP_HOME + exit 1 +else + echo "OK" +fi + +rm -f $LINK + +## +# test long name from config +## +echo "> test default long name from compiled-in value" +RESULT=$($COPIED) +echo "$RESULT" | grep -q "Command Launcher - A command launcher" +if [ $? -ne 0 ]; then + echo "KO - expected compiled-in long name as default" + rm -f $COPIED + rm -rf $MYAPP_HOME + exit 1 +else + echo "OK" +fi + +echo "> test long name override from config" +$COPIED config app_long_name "My Custom App" + +RESULT=$($COPIED) +echo "$RESULT" | grep -q "My Custom App - A command launcher" +if [ $? -ne 0 ]; then + echo "KO - expected long name from config" + rm -f $COPIED + rm -rf $MYAPP_HOME + exit 1 +else + echo "OK" +fi + +## +# test original binary is unaffected by copy's config +## +echo "> test original binary unaffected by copy's config" +RESULT=$($CL_PATH) +echo "$RESULT" | grep -q "Command Launcher - A command launcher" +if [ $? -ne 0 ]; then + echo "KO - original should still use compiled-in long name" + rm -f $COPIED + rm -rf $MYAPP_HOME + exit 1 +else + echo "OK" +fi + +# cleanup +rm -f $COPIED +rm -rf $MYAPP_HOME From 2b48e18fbe925ae6d0b96e90383bae9ee51aa564 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Mon, 23 Mar 2026 23:49:46 +0000 Subject: [PATCH 2/5] Update FULL_COMMAND_NAME test to not rely on binary copies sharing identity The test-cmd-context test previously copied the cl binary to clcopy and checked that CL_FULL_COMMAND_NAME reflected the new name. With runtime app name resolution, a copied binary is now a separate instance with its own env var prefix (CLCOPY_ instead of CL_), so the dropin script's hardcoded $CL_FULL_COMMAND_NAME comes back empty. The copy-based FULL_COMMAND_NAME behavior is already covered by the new test-runtime-name integration test. This test now checks FULL_COMMAND_NAME with the original cl binary, which still validates that the env var is correctly set with group and without group. --- test/integration/test-cmd-context.sh | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/integration/test-cmd-context.sh b/test/integration/test-cmd-context.sh index 95b01fd..46f9beb 100755 --- a/test/integration/test-cmd-context.sh +++ b/test/integration/test-cmd-context.sh @@ -89,13 +89,9 @@ else exit 1 fi -# Make a copy and run the copy to ensure FULL_COMMAND_NAME starts -# with the name of the actual executable that runs the launcher -cp "$OUTPUT_DIR/"{cl,clcopy} - echo "> test FULL_COMMAND_NAME environment variable (with group)" -RESULT=$("$OUTPUT_DIR"/clcopy greeting saybonjour) -echo "$RESULT" | grep -q "^command name: clcopy greeting saybonjour$" +RESULT=$("$CL_PATH" greeting saybonjour) +echo "$RESULT" | grep -q "^command name: cl greeting saybonjour$" if [ $? -eq 0 ]; then echo "OK" else @@ -104,8 +100,8 @@ else fi echo "> test FULL_COMMAND_NAME environment variable (no group)" -RESULT=$("$OUTPUT_DIR"/clcopy bonjour) -echo "$RESULT" | grep -q "^command name: clcopy bonjour$" +RESULT=$("$CL_PATH" bonjour) +echo "$RESULT" | grep -q "^command name: cl bonjour$" if [ $? -eq 0 ]; then echo "OK" else From 034e85c4987ff373953d6e2a9d5f9324222637f3 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Mon, 23 Mar 2026 23:55:29 +0000 Subject: [PATCH 3/5] Skip symlink test on Windows where symlinks need elevated privileges --- test/integration/test-runtime-name.sh | 31 +++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/test/integration/test-runtime-name.sh b/test/integration/test-runtime-name.sh index d6d96b2..0f995ed 100755 --- a/test/integration/test-runtime-name.sh +++ b/test/integration/test-runtime-name.sh @@ -40,25 +40,28 @@ else fi ## -# test symlink resolves to original name +# test symlink resolves to original name (skip on Windows, symlinks need elevated privileges) ## -echo "> test symlink resolves to original binary name" -LINK=$OUTPUT_DIR/myalias -ln -sf $CL_PATH $LINK +if ln -sf $CL_PATH $OUTPUT_DIR/myalias 2>/dev/null; then + echo "> test symlink resolves to original binary name" + LINK=$OUTPUT_DIR/myalias -RESULT=$($LINK version) -echo "$RESULT" | grep -q "^cl version" -if [ $? -ne 0 ]; then - echo "KO - symlink should resolve to original name 'cl'" - rm -f $LINK $COPIED - rm -rf $MYAPP_HOME - exit 1 + RESULT=$($LINK version) + echo "$RESULT" | grep -q "^cl version" + if [ $? -ne 0 ]; then + echo "KO - symlink should resolve to original name 'cl'" + rm -f $LINK $COPIED + rm -rf $MYAPP_HOME + exit 1 + else + echo "OK" + fi + + rm -f $LINK else - echo "OK" + echo "> test symlink resolves to original binary name - SKIPPED (symlinks not available)" fi -rm -f $LINK - ## # test long name from config ## From 5ce0d508f6bc91c1959a93699b7573d103b3759f Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Wed, 25 Mar 2026 20:21:18 +0000 Subject: [PATCH 4/5] Skip symlink test on Windows where Git Bash ln -s creates copies --- test/integration/test-runtime-name.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/integration/test-runtime-name.sh b/test/integration/test-runtime-name.sh index 0f995ed..819c4c4 100755 --- a/test/integration/test-runtime-name.sh +++ b/test/integration/test-runtime-name.sh @@ -40,11 +40,16 @@ else fi ## -# test symlink resolves to original name (skip on Windows, symlinks need elevated privileges) +# test symlink resolves to original name +# Skip on Windows: Git Bash's ln -s creates a copy rather than a real symlink, +# so EvalSymlinks cannot resolve back to the original binary. ## -if ln -sf $CL_PATH $OUTPUT_DIR/myalias 2>/dev/null; then +if [ "$(uname -o 2>/dev/null)" = "Msys" ] || [ "$(uname -o 2>/dev/null)" = "MS/Windows" ]; then + echo "> test symlink resolves to original binary name - SKIPPED (Windows)" +else echo "> test symlink resolves to original binary name" LINK=$OUTPUT_DIR/myalias + ln -sf $CL_PATH $LINK RESULT=$($LINK version) echo "$RESULT" | grep -q "^cl version" @@ -58,8 +63,6 @@ if ln -sf $CL_PATH $OUTPUT_DIR/myalias 2>/dev/null; then fi rm -f $LINK -else - echo "> test symlink resolves to original binary name - SKIPPED (symlinks not available)" fi ## From 2dd522cd4fee85d534faa457965218497eb4e8c7 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Fri, 3 Apr 2026 18:28:17 +0100 Subject: [PATCH 5/5] Document renaming the binary as an alternative to building from source --- README.md | 11 ++++++++++- gh-pages/content/en/docs/overview/introduction.md | 13 ++++++++++++- .../content/en/docs/quickstart/build-from-source.md | 11 ++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c8d30eb..9a633b7 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,16 @@ Developers run command launcher to access these commands, for example, you have Pre-built binary can be downloaded from the release page. Unzip it, copy the binary into your PATH. -The pre-built binary is named `cdt` (Criteo Dev Toolkit), if you want to use a different name, you can pass your prefered name in the build. See build section below. +The pre-built binary is named `cdt` (Criteo Dev Toolkit). To use a different name, just copy or rename the binary — the app name is derived from the binary's file name at startup: + +``` +cp cdt myapp +myapp config app_long_name "My App" +``` + +Symlinks are treated as aliases (they resolve to the original binary name), while copies create a separate instance with its own config directory (`~/.myapp/`). + +You can also set the name at build time if you prefer. See the build section below. ## Contribute diff --git a/gh-pages/content/en/docs/overview/introduction.md b/gh-pages/content/en/docs/overview/introduction.md index 22ff513..f9d1e75 100644 --- a/gh-pages/content/en/docs/overview/introduction.md +++ b/gh-pages/content/en/docs/overview/introduction.md @@ -72,11 +72,22 @@ A pre-built binary can be downloaded from the release page. Unzip it, and place The two pre-built binaries are named `cola` (**Co**mmand **La**uncher) and `cdt` (**C**riteo **D**ev **T**oolkit), if you want to use a different name, you can pass your preferred name in the build. See the *build* section below. +## Using a custom name + +The easiest way to use a custom name is to copy or rename the pre-built binary. The app name is derived from the binary's file name at startup: + +```shell +cp cdt myapp +myapp config app_long_name "My App" +``` + +Symlinks are treated as aliases (they resolve to the original binary name), while copies create a separate instance with its own config directory. + ## Building Requirements: golang >= 1.17 -You can build the command launcher with your preferred name (in the example: `Criteo Developer Toolkit`, a.k.a `cdt`). +You can also set the name at build time (in the example: `Criteo Developer Toolkit`, a.k.a `cdt`). ```shell go build -o cdt -ldflags='-X main.version=dev -X main.appName=cdt -X "main.appLongName=Criteo Dev Toolkit"' main.go diff --git a/gh-pages/content/en/docs/quickstart/build-from-source.md b/gh-pages/content/en/docs/quickstart/build-from-source.md index 8bc8816..95af088 100644 --- a/gh-pages/content/en/docs/quickstart/build-from-source.md +++ b/gh-pages/content/en/docs/quickstart/build-from-source.md @@ -24,7 +24,16 @@ Another pre-built binary is called `cdt` (Criteo Dev Toolkit), its home folder w > For compatibility concern, we highly recommend to reference resources in your command with prefix `COLA_` -To use a different name, you need to build command launcher from source and pass the desired short and long name to the build scripts. +The easiest way to use a different name is to simply copy or rename the pre-built binary. The app name is derived from the binary's file name at startup. You can then set the long display name via config: + +```shell +cp cola myapp +myapp config app_long_name "My App" +``` + +Symlinks are treated as aliases (they resolve to the original binary name), while copies create a separate instance with its own config directory (`~/.myapp/`). + +You can also set the name at build time if you prefer. ## Build from source