Skip to content

Commit d2fb205

Browse files
authored
feat: add JULIA_EXE env var for custom Julia binary (#18)
* feat: add JULIA_EXE env var to specify Julia binary * cleanup: remove juliaExe field from struct; trim skill docs * fix: add missing logFile arg to test newJuliaSession calls
1 parent 956478a commit d2fb205

6 files changed

Lines changed: 47 additions & 21 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ Or manually by adding this repo's `skills/` directory to your Agent skill search
3131
# Evaluate code (daemon starts automatically)
3232
julia-client -e 'println("hello")'
3333

34+
# Use a custom Julia binary via JULIA_EXE environment variable
35+
JULIA_EXE=/path/to/julia julia-client -e 'println("hello")'
36+
3437
# Explicit project: each distinct --project is its own session
3538
julia-client --project /path/to/project -e 'using MyPackage'
3639
julia-client --project @temp -e 'using Pkg; Pkg.add("Example")'

go/daemon.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func handleStreamingEval(state *daemonState, req protocolRequest, conn net.Conn)
166166
if req.Fresh {
167167
state.manager.restart(req.Session, req.Project, req.Cwd)
168168
}
169-
sess, err := state.manager.getOrCreate(req.Cwd, req.Project, req.Session, req.JuliaArgs)
169+
sess, err := state.manager.getOrCreate(req.Cwd, req.Project, req.Session, req.JuliaExe, req.JuliaArgs)
170170
if err != nil {
171171
emit(streamFrame{Done: true, Error: err.Error()})
172172
return

go/main.go

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type protocolRequest struct {
5959
Project string `json:"project,omitempty"`
6060
Session string `json:"session,omitempty"`
6161
JuliaArgs []string `json:"julia_args,omitempty"` // extra switches forwarded to the julia subprocess
62+
JuliaExe string `json:"julia_exe,omitempty"` // custom Julia binary path
6263
PrintResult bool `json:"print_result,omitempty"`
6364
Fresh bool `json:"fresh,omitempty"`
6465
TraceLevel string `json:"trace_level,omitempty"`
@@ -146,7 +147,7 @@ func normalizeProjectArg(project string) string {
146147
return projectArg
147148
}
148149

149-
func cmdEval(socketPath, code, project, session string, printResult, fresh bool, traceLevel string, juliaArgs []string) {
150+
func cmdEval(socketPath, code, project, session, juliaExe string, printResult, fresh bool, traceLevel string, juliaArgs []string) {
150151
if code == "-" {
151152
b, err := io.ReadAll(os.Stdin)
152153
if err != nil {
@@ -161,6 +162,7 @@ func cmdEval(socketPath, code, project, session string, printResult, fresh bool,
161162
Cwd: mustGetwd(),
162163
Project: normalizeProjectArg(project),
163164
Session: session,
165+
JuliaExe: juliaExe,
164166
TraceLevel: traceLevel,
165167
JuliaArgs: juliaArgs,
166168
PrintResult: printResult,
@@ -249,6 +251,7 @@ var subcommands = map[string]bool{
249251
// passthrough switches for the Julia subprocess.
250252
type parsed struct {
251253
socket string
254+
juliaExe string
252255
project string
253256
session string
254257
trace string
@@ -270,16 +273,17 @@ type parsed struct {
270273
func parseArgs(args []string) parsed {
271274
// Canonical name (dashes/value stripped) -> setter on p.
272275
value := map[string]func(*parsed, string){
273-
"e": func(p *parsed, v string) { p.evalMode, p.code = "eval", v },
274-
"eval": func(p *parsed, v string) { p.evalMode, p.code = "eval", v },
275-
"E": func(p *parsed, v string) { p.evalMode, p.code = "print", v },
276-
"print": func(p *parsed, v string) { p.evalMode, p.code = "print", v },
277-
"socket": func(p *parsed, v string) { p.socket = v },
278-
"project": func(p *parsed, v string) { p.project = v },
279-
"session": func(p *parsed, v string) { p.session = v },
280-
"trace": func(p *parsed, v string) { p.trace = v },
281-
"t": func(p *parsed, v string) { p.threads = v },
282-
"threads": func(p *parsed, v string) { p.threads = v },
276+
"e": func(p *parsed, v string) { p.evalMode, p.code = "eval", v },
277+
"eval": func(p *parsed, v string) { p.evalMode, p.code = "eval", v },
278+
"E": func(p *parsed, v string) { p.evalMode, p.code = "print", v },
279+
"print": func(p *parsed, v string) { p.evalMode, p.code = "print", v },
280+
"socket": func(p *parsed, v string) { p.socket = v },
281+
282+
"project": func(p *parsed, v string) { p.project = v },
283+
"session": func(p *parsed, v string) { p.session = v },
284+
"trace": func(p *parsed, v string) { p.trace = v },
285+
"t": func(p *parsed, v string) { p.threads = v },
286+
"threads": func(p *parsed, v string) { p.threads = v },
283287
}
284288

285289
p := parsed{socket: defaultSocket, project: "@."}
@@ -342,24 +346,27 @@ func main() {
342346
juliaArgs = append(juliaArgs, "-t", t)
343347
}
344348

349+
// JULIA_EXE selects the Julia binary; empty means look up "julia" in PATH.
350+
juliaExe := os.Getenv("JULIA_EXE")
351+
345352
switch {
346353
case p.evalMode != "":
347-
cmdEval(p.socket, p.code, p.project, p.session, p.evalMode == "print", p.fresh, p.trace, juliaArgs)
354+
cmdEval(p.socket, p.code, p.project, p.session, juliaExe, p.evalMode == "print", p.fresh, p.trace, juliaArgs)
348355
case len(p.files) > 0:
349356
f := p.files[0]
350357
if _, err := os.Stat(f); err != nil {
351358
fmt.Fprintf(os.Stderr, "unknown command: %s\n", f)
352359
usage()
353360
}
354361
code := fmt.Sprintf("cd(%q) do; Base.include(Main, %q); end", mustGetwd(), f)
355-
cmdEval(p.socket, code, p.project, p.session, false, p.fresh, p.trace, juliaArgs)
362+
cmdEval(p.socket, code, p.project, p.session, juliaExe, false, p.fresh, p.trace, juliaArgs)
356363
default:
357364
// No code given: read stdin only if it's a pipe/redirect, not a terminal.
358365
fi, err := os.Stdin.Stat()
359366
if err != nil || fi.Mode()&os.ModeCharDevice != 0 {
360367
usage()
361368
}
362-
cmdEval(p.socket, "-", p.project, p.session, false, p.fresh, p.trace, juliaArgs)
369+
cmdEval(p.socket, "-", p.project, p.session, juliaExe, false, p.fresh, p.trace, juliaArgs)
363370
}
364371
}
365372

go/manager.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (m *SessionManager) openLogFile(key string) *os.File {
5656

5757
// juliaArgs apply only when a session is first created; a live session for the
5858
// key is reused as-is regardless (use --fresh to rebuild with new args).
59-
func (m *SessionManager) getOrCreate(cwd, project, session string, juliaArgs []string) (*JuliaSession, error) {
59+
func (m *SessionManager) getOrCreate(cwd, project, session, juliaExe string, juliaArgs []string) (*JuliaSession, error) {
6060
key := m.key(session, project, cwd)
6161

6262
// Fast path: return existing live session without singleflight overhead.
@@ -87,7 +87,7 @@ func (m *SessionManager) getOrCreate(cwd, project, session string, juliaArgs []s
8787
projectVal = "@."
8888
}
8989
sess = newJuliaSession(projectVal, newSentinel(), juliaArgs, m.openLogFile(key))
90-
if err := sess.start(cwd); err != nil {
90+
if err := sess.start(juliaExe, cwd); err != nil {
9191
return nil, err
9292
}
9393

go/session.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"io"
1010
"os"
1111
"os/exec"
12+
"path/filepath"
1213
"strings"
1314
"sync"
1415
"sync/atomic"
@@ -55,10 +56,21 @@ func newJuliaSession(projectVal, sentinel string, juliaArgs []string, logFile *o
5556
}
5657
}
5758

58-
func (s *JuliaSession) start(workDir string) error {
59-
exe, err := exec.LookPath("julia")
60-
if err != nil {
61-
return fmt.Errorf("'julia' not found in PATH. Install Julia from https://julialang.org/downloads/")
59+
func (s *JuliaSession) start(juliaExe string, workDir string) error {
60+
var exe string
61+
if juliaExe != "" {
62+
if filepath.IsAbs(juliaExe) {
63+
exe = juliaExe
64+
} else {
65+
// Relative path: resolved relative to the project directory
66+
exe = filepath.Join(s.projectVal, juliaExe)
67+
}
68+
} else {
69+
var err error
70+
exe, err = exec.LookPath("julia")
71+
if err != nil {
72+
return fmt.Errorf("'julia' not found in PATH. Install Julia from https://julialang.org/downloads/")
73+
}
6274
}
6375

6476
// A juliaup channel (+x) must be Julia's very first argument; keep it there.

skills/julia-client/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Treat `julia-client` like persistent REPL: run setup once with `julia-client -E
1515
- struct/type redefinition
1616
- adding `using NewPkg` inside modules whose `Project.toml` didn't already list `NewPkg` dep (the module's load-time dep view is cached so `Pkg.add`+edit isn't enough)
1717

18+
## Environment
19+
20+
- `JULIA_EXE` — Path to the Julia binary. If unset, `julia` is looked up in `$PATH`.
21+
1822
## Other Examples
1923

2024
```bash

0 commit comments

Comments
 (0)