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
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ APP := imsg-bridge
CLI := imsg-bridge-cli
VERSION := $(shell tr -d '\n' < VERSION)
BUILD_LDFLAGS := -X github.com/kacy/imsg-bridge/internal/buildinfo.Version=$(VERSION)
IMSG_BIN ?= $(shell [ -x ../imsg/bin/imsg ] && printf '%s' ../imsg/bin/imsg || printf '%s' imsg)

.PHONY: build build-cli test fmt run clean dist release release-patch release-minor version web-install web-dev web-build web-test

Expand All @@ -20,7 +21,7 @@ fmt:
gofmt -w ./cmd ./internal

run:
go run -ldflags "$(BUILD_LDFLAGS)" ./cmd/imsg-bridge
go run -ldflags "$(BUILD_LDFLAGS)" ./cmd/imsg-bridge -imsg-bin "$(IMSG_BIN)"

clean:
rm -rf bin dist web/dist web/*.tsbuildinfo web/vite.config.js web/vite.config.d.ts web/tailwind.config.js web/tailwind.config.d.ts
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ still landing:
- [`imsg`](https://github.com/steipete/imsg): `brew install imsg`
- [tailscale](https://tailscale.com/) installed and connected

for local bridge work, the daemon also accepts `IMSGBRIDGE_IMSG_BIN=/path/to/imsg` or `-imsg-bin /path/to/imsg` if you need to point at a patched checkout before upstream catches up.

## quick start

```sh
Expand Down
5 changes: 4 additions & 1 deletion cmd/imsg-bridge/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ import (
func main() {
var cfg api.Config
var dataDir string
var imsgBinary string
var socketPath string

flag.StringVar(&cfg.ListenAddr, "listen", ":8443", "http listen address")
flag.StringVar(&cfg.ServerName, "server-name", hostname(), "server name")
flag.StringVar(&cfg.TailscaleIP, "tailscale-ip", "", "override detected tailscale ip")
flag.StringVar(&dataDir, "data-dir", defaultDataDir(), "data directory")
flag.StringVar(&imsgBinary, "imsg-bin", "", "path to the imsg binary")
flag.StringVar(&socketPath, "socket", "", "control socket path")
flag.Parse()

Expand All @@ -58,7 +60,7 @@ func main() {
log.Fatal(err)
}

runner := imsg.NewRunner("")
runner := imsg.NewRunner(imsgBinary)
hub := events.NewHub()
stateStore := store.New(filepath.Join(dataDir, "config.json"))
state, err := stateStore.Load()
Expand Down Expand Up @@ -154,6 +156,7 @@ func main() {
}()

log.Printf("serving https on %s", cfg.ListenAddr)
log.Printf("imsg binary %s", runner.Binary())
log.Printf("control socket %s", socketPath)
log.Printf("tls fingerprint %s", material.Fingerprint)
logPairingWarnings(cfg.ListenAddr, cfg.TailscaleIP, pairHost)
Expand Down
53 changes: 50 additions & 3 deletions internal/imsg/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
)
Expand All @@ -19,11 +21,56 @@ type Runner struct {
}

func NewRunner(binary string) *Runner {
if strings.TrimSpace(binary) == "" {
binary = "imsg"
return &Runner{binary: ResolveBinary(binary)}
}

func ResolveBinary(binary string) string {
if binary := strings.TrimSpace(binary); binary != "" {
return binary
}

if binary := strings.TrimSpace(os.Getenv("IMSGBRIDGE_IMSG_BIN")); binary != "" {
return binary
}

for _, candidate := range defaultBinaryCandidates() {
if isExecutableFile(candidate) {
return candidate
}
}

return "imsg"
}

func (r *Runner) Binary() string {
return r.binary
}

func defaultBinaryCandidates() []string {
candidates := make([]string, 0, 3)

if cwd, err := os.Getwd(); err == nil {
candidates = append(candidates, filepath.Join(cwd, "..", "imsg", "bin", "imsg"))
}

if exePath, err := os.Executable(); err == nil {
exeDir := filepath.Dir(exePath)
candidates = append(candidates,
filepath.Join(exeDir, "..", "imsg", "bin", "imsg"),
filepath.Join(exeDir, "..", "..", "imsg", "bin", "imsg"),
)
}

return candidates
}

func isExecutableFile(path string) bool {
info, err := os.Stat(path)
if err != nil || info.IsDir() {
return false
}

return &Runner{binary: binary}
return info.Mode()&0o111 != 0
}

func (r *Runner) Version(ctx context.Context) (string, error) {
Expand Down
79 changes: 79 additions & 0 deletions internal/imsg/runner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package imsg

import (
"os"
"path/filepath"
"testing"
)

func TestResolveBinaryPrefersExplicitPath(t *testing.T) {
t.Setenv("IMSGBRIDGE_IMSG_BIN", "/tmp/from-env")

if got := ResolveBinary("/tmp/from-flag"); got != "/tmp/from-flag" {
t.Fatalf("expected explicit path, got %q", got)
}
}

func TestResolveBinaryPrefersEnvVar(t *testing.T) {
t.Setenv("IMSGBRIDGE_IMSG_BIN", "/tmp/from-env")

if got := ResolveBinary(""); got != "/tmp/from-env" {
t.Fatalf("expected env path, got %q", got)
}
}

func TestResolveBinaryFindsSiblingCheckoutFromWorkingDir(t *testing.T) {
root := t.TempDir()
bridgeDir := filepath.Join(root, "imsg-bridge")
imsgDir := filepath.Join(root, "imsg", "bin")

if err := os.MkdirAll(bridgeDir, 0o755); err != nil {
t.Fatalf("mkdir bridge dir: %v", err)
}
if err := os.MkdirAll(imsgDir, 0o755); err != nil {
t.Fatalf("mkdir imsg dir: %v", err)
}

imsgPath := filepath.Join(imsgDir, "imsg")
if err := os.WriteFile(imsgPath, []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatalf("write imsg binary: %v", err)
}

prevWD, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
t.Cleanup(func() {
if chdirErr := os.Chdir(prevWD); chdirErr != nil {
t.Fatalf("restore wd: %v", chdirErr)
}
})

if err := os.Chdir(bridgeDir); err != nil {
t.Fatalf("chdir: %v", err)
}

t.Setenv("IMSGBRIDGE_IMSG_BIN", "")

got := ResolveBinary("")
resolvedGot, err := filepath.EvalSymlinks(got)
if err != nil {
t.Fatalf("resolve got path: %v", err)
}
resolvedWant, err := filepath.EvalSymlinks(imsgPath)
if err != nil {
t.Fatalf("resolve want path: %v", err)
}

if resolvedGot != resolvedWant {
t.Fatalf("expected sibling checkout path %q, got %q", resolvedWant, resolvedGot)
}
}

func TestResolveBinaryFallsBackToPathLookup(t *testing.T) {
t.Setenv("IMSGBRIDGE_IMSG_BIN", "")

if got := ResolveBinary(""); got != "imsg" {
t.Fatalf("expected default binary name, got %q", got)
}
}
Loading