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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 21 additions & 17 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
13 changes: 12 additions & 1 deletion gh-pages/content/en/docs/overview/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion gh-pages/content/en/docs/quickstart/build-from-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions internal/config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -81,6 +82,7 @@ func init() {
ENABLE_PACKAGE_SETUP_HOOK_KEY,
GROUP_HELP_BY_REGISTRY_KEY,
ENABLE_WORKSPACE_PACKAGES_KEY,
APP_LONG_NAME_KEY,
)
}

Expand Down Expand Up @@ -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)
Expand Down
29 changes: 28 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package main

import (
"os"
"path/filepath"
"strings"

root "github.com/jdevera/command-launcher/cmd"
)

Expand All @@ -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()
}
64 changes: 64 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -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))])
}
12 changes: 4 additions & 8 deletions test/integration/test-cmd-context.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
114 changes: 114 additions & 0 deletions test/integration/test-runtime-name.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/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
# 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 [ "$(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"
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
fi

##
# 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
Loading