From af34385f95e6b9d5df65f7bbcd8854ee7814a958 Mon Sep 17 00:00:00 2001 From: nicodes Date: Tue, 27 Oct 2026 08:52:12 +0100 Subject: [PATCH] Add standalone shell installer --- README.md | 6 +-- cmd/spritesmith/installer_test.go | 68 +++++++++++++++++++++++++++++++ install.sh | 58 ++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 cmd/spritesmith/installer_test.go create mode 100755 install.sh diff --git a/README.md b/README.md index 8b2bb59..70351e4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + # spritesmith @@ -8,10 +8,10 @@ The client requires an API service and an account token. It does not contain a l ## Install -With Go 1.27.2 or later: +With Go 1.27.2 or later, install to `~/.local/bin` (add it to your PATH if necessary): ```sh -go install github.com/aviorstudio/spritesmith/cmd/spritesmith@latest +curl -fsSL https://spritesmith.ai/install | sh ``` ## Configure diff --git a/cmd/spritesmith/installer_test.go b/cmd/spritesmith/installer_test.go new file mode 100644 index 0000000..3e805f1 --- /dev/null +++ b/cmd/spritesmith/installer_test.go @@ -0,0 +1,68 @@ +//go:build !windows + +package main + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func TestCurlInstaller(t *testing.T) { + _, file, _, _ := runtime.Caller(0) + script := filepath.Join(filepath.Dir(file), "../../install.sh") + for _, fail := range []bool{false, true} { + name := "success" + if fail { + name = "failed build preserves existing executable" + } + t.Run(name, func(t *testing.T) { + root := t.TempDir() + bin, fake := filepath.Join(root, "bin"), filepath.Join(root, "commands") + for _, dir := range []string{bin, fake} { + if err := os.Mkdir(dir, 0700); err != nil { + t.Fatal(err) + } + } + if err := os.WriteFile(filepath.Join(bin, "spritesmith"), []byte("previous executable\n"), 0700); err != nil { + t.Fatal(err) + } + goCommand := `#!/bin/sh +set -eu +[ "$1" = install ] +[ "$2" = 'github.com/aviorstudio/spritesmith/cmd/spritesmith@latest' ] +printf 'new executable\n' > "$GOBIN/spritesmith" +if [ "${INSTALL_BUILD_FAIL:-0}" = 1 ]; then exit 42; fi +` + if err := os.WriteFile(filepath.Join(fake, "go"), []byte(goCommand), 0700); err != nil { + t.Fatal(err) + } + cmd := exec.Command("sh", script, "--install-dir", bin) + cmd.Env = append(os.Environ(), "PATH="+fake+string(os.PathListSeparator)+os.Getenv("PATH")) + if fail { + cmd.Env = append(cmd.Env, "INSTALL_BUILD_FAIL=1") + } + output, err := cmd.CombinedOutput() + if fail == (err == nil) { + t.Fatalf("unexpected result: %v\n%s", err, output) + } + body, err := os.ReadFile(filepath.Join(bin, "spritesmith")) + if err != nil { + t.Fatal(err) + } + want := "new executable\n" + if fail { + want = "previous executable\n" + } + if string(body) != want { + t.Fatalf("unexpected executable: %q", body) + } + if !fail && !strings.Contains(string(output), "Installed spritesmith") { + t.Fatalf("missing install receipt: %s", output) + } + }) + } +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..2df5b9c --- /dev/null +++ b/install.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# Install spritesmith using the Go module's normal verification and build process. +set -eu + +tool=spritesmith +module=github.com/aviorstudio/spritesmith/cmd/spritesmith +release=latest +install_dir=${INSTALL_DIR:-"$HOME/.local/bin"} +work_dir= +stage_dir= +fail() { printf '%s: %s\n' "$tool" "$*" >&2; exit 1; } +cleanup() { + [ -z "$work_dir" ] || rm -rf "$work_dir" + [ -z "$stage_dir" ] || rm -rf "$stage_dir" +} +trap cleanup 0 +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM +while [ "$#" -gt 0 ]; do + case "$1" in + --version|-v|--install-dir) + [ "$#" -ge 2 ] || fail "$1 needs a value." + case "$1" in + --version|-v) release=$2 ;; + --install-dir) install_dir=$2 ;; + esac + shift 2 ;; + --help|-h) + printf 'Install %s with Go: [--version VERSION|latest] [--install-dir /absolute/path]\nDefaults: latest Go module, ~/.local/bin. Requires Go.\n' "$tool" + exit 0 ;; + *) fail "Unknown option: $1" ;; + esac +done +case "$install_dir" in /*) ;; *) fail 'The install directory must be an absolute path.' ;; esac +command -v go >/dev/null 2>&1 || fail 'Install Go first, then run this command again.' +case "$release" in + latest) ;; + v[0-9]*) ;; + [0-9]*) release=v$release ;; + *) fail 'Use latest or a Go module version such as v1.2.3.' ;; +esac +work_dir=$(mktemp -d "${TMPDIR:-/tmp}/$tool-install.XXXXXXXX") || fail 'Could not create a temporary directory.' +mkdir "$work_dir/bin" +printf 'Building %s from %s@%s...\n' "$tool" "$module" "$release" +GOBIN="$work_dir/bin" go install "$module@$release" || fail 'Go installation failed; the installed binary was not changed.' +[ -f "$work_dir/bin/$tool" ] && [ ! -L "$work_dir/bin/$tool" ] || fail 'Go did not produce a regular executable.' +mkdir -p "$install_dir" || fail 'Choose a writable install directory.' +[ ! -d "$install_dir/$tool" ] || fail 'The install destination is a directory.' +stage_dir=$(mktemp -d "$install_dir/.$tool-install.XXXXXXXX") || fail 'Could not stage the executable.' +cp "$work_dir/bin/$tool" "$stage_dir/$tool" +chmod 755 "$stage_dir/$tool" +mv -f "$stage_dir/$tool" "$install_dir/$tool" +printf 'Installed %s at %s/%s\n' "$tool" "$install_dir" "$tool" +case ":$PATH:" in + *":$install_dir:"*) ;; + *) printf 'Add %s to your PATH to run %s by name.\n' "$install_dir" "$tool" ;; +esac