Skip to content

Commit 264e950

Browse files
lreadborkdude
andauthored
Fix babashka#163, babashka#164 Change win program resolution to match macOS/Linux (babashka#165)
Co-authored-by: Michiel Borkent <[email protected]>
1 parent ae1656b commit 264e950

File tree

16 files changed

+619
-121
lines changed

16 files changed

+619
-121
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
[Babashka process](https://github.com/babashka/process)
44
Clojure library for shelling out / spawning sub-processes
55

6+
## Unreleased
7+
8+
- [#163](https://github.com/babashka/process/issues/163), [#164](https://github.com/babashka/process/issues/164): Program resolution strategy for `exec` and Windows now matches macOS/Linux/PowerShell ([@lread](https://github.com/lread))
9+
610
## 0.5.22 (2024-02-29)
711

812
- [#123](https://github.com/babashka/process/issues/123): `exec` now converts `:env` and `:extra-env` keywords ([@lread](https://github.com/lread))

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ ls: nothing: No such file or directory
9696
Execution error (ExceptionInfo) at babashka.process/check (process.cljc:113).
9797
```
9898

99-
To avoid throwing when the command's exit code is non-zero, use `:continue true`.
99+
To avoid throwing when the command's exit code is non-zero, use `:continue true`.
100100
You will still see the error printed to stderr, but no exception will be thrown. This is convenient
101101
when you want to handle the `:exit` code yourself:
102102

@@ -490,6 +490,31 @@ nil
490490
491491
Another solution is to let bash handle the pipes by shelling out with `bash -c`.
492492
493+
## Program Resolution
494+
495+
### macOS & Linux
496+
On macOS & Linux, programs are resolved the way you expect:
497+
498+
- `a` resolves against the system `PATH`
499+
- `./a` resolves against `:dir` if specified, otherwise the current working directory
500+
- `/some/absolute/a` resolves absolutely
501+
502+
In all cases, the working directory for `a` is `:dir`, if specified, otherwise your current working directory.
503+
504+
### Windows
505+
506+
Windows executable files have extensions, which, if not specified, are resolved in order: `.com`,`.exe`,`.bat`,`.cmd`.
507+
Programs are resolved in directories using the same rules as macOS, Linux, and Windows PowerShell.
508+
509+
> **Windows .ps1 TIP**: Babashka process will never resolve to, and cannot launch, `.ps1` scripts directly.
510+
To launch a `.ps1` script, you must do so through PowerShell.
511+
Example:
512+
> ```Clojure
513+
> (p/shell "powershell.exe -File .\\a.ps1")
514+
> ```
515+
516+
> **Windows TIP**: If you prefer a more CMD Shell-like experience where programs are resolved first in the current working directory, then on the `PATH`, and are OK with the security implications of doing so, you can override the default `:program-resolver` with your own.
517+
493518
## Differences with `clojure.java.shell/sh`
494519
495520
If `clojure.java.shell` works for your purposes, keep using it. But there are

bb.edn

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,58 @@
33
#_{:local/root "../../quickdoc"}
44
{:git/url "https://github.com/borkdude/quickdoc"
55
:git/sha "32e726cd6d785d00e49d4e614a05f7436d3831c0"}
6-
org.clj-commons/digest {:mvn/version "1.4.100"} }
6+
org.clj-commons/digest {:mvn/version "1.4.100"}}
7+
78
:tasks
8-
{:requires ([babashka.fs :as fs])
9+
{:requires ([babashka.cli :as cli]
10+
[babashka.fs :as fs])
11+
:init (do
12+
(def shell-opts {:extra-env
13+
{(if (fs/windows?) "Path" "PATH")
14+
(str (fs/canonicalize "target/test/on-path") fs/path-separator (System/getenv "PATH"))}})
15+
(defn parse-repl-args [args]
16+
(let [cli-spec {:spec
17+
{:host {:desc "Bind to host (use 0.0.0.0 to allow anyone to connect)"
18+
:alias :h
19+
:default "localhost"}}
20+
:restrict true}]
21+
(cli/parse-opts args cli-spec))))
922
clean {:doc "Delete build work"
1023
:task (do
1124
(fs/delete-tree "target")
1225
(shell {:dir "test-native"} "bb clean"))}
26+
27+
dev:jvm {:doc "Start a jvm nREPL server with PATH appropriatly setup for tests"
28+
:task (let [opts (parse-repl-args *command-line-args*)
29+
host (:host opts)]
30+
(shell shell-opts
31+
"clj" "-M:clj-1.11:test:nrepl/jvm" "-h" host "-b" host))}
32+
33+
-bb {:doc "Internal - launched by dev:bb, test:bb"
34+
:extra-paths ["test"]
35+
:extra-deps {;; inherit base deps from deps.edn
36+
babashka/process {:local/root "."}
37+
;; repeat necessary :test deps from deps.edn
38+
io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"}}
39+
:requires ([cognitect.test-runner])
40+
:task (let [target (cli/coerce (first *command-line-args*) :keyword) ;; should be either :test or :dev
41+
args (rest *command-line-args*)]
42+
;; flag: sub-process should reload babashka.process
43+
(System/setProperty "babashka.process.test.reload" "true")
44+
;; flag: force use of bb even if natively compiled version of run-exec exists
45+
(System/setProperty "babashka.process.test.run-exec" "bb")
46+
;; run from babashka.process sources, not built-in babaska.process
47+
(require '[babashka.process] :reload)
48+
(case target
49+
:test (apply cognitect.test-runner.-main args)
50+
:dev (let [opts (parse-repl-args args)]
51+
(babashka.nrepl.server/start-server! opts)
52+
(deref (promise)))))}
53+
54+
dev:bb {:doc "Start a bb nREPL server with PATH appropriately setup for tests"
55+
:task (apply shell shell-opts
56+
"bb -bb :dev" *command-line-args*)}
57+
1358
quickdoc {:doc "Invoke quickdoc"
1459
:requires ([quickdoc.api :as api])
1560
:task (api/quickdoc {:git/branch "master"
@@ -22,23 +67,12 @@
2267

2368
test:native {:doc "Run exec tests with native runner (requires GraalVM compilation)."
2469
:depends [-prep-native-exec]
25-
:task (apply clojure "-M:test:clj-1.11" "--namespace" "babashka.process-exec-test" *command-line-args*)}
70+
:task (apply clojure shell-opts
71+
"-M:test:clj-1.11" "--namespace" "babashka.process-exec-test" *command-line-args*)}
2672

2773
test:bb {:doc "Run all tests under bb"
28-
:extra-paths ["test"]
29-
:extra-deps {;; inherit base deps from deps.edn
30-
babashka/process {:local/root "."}
31-
;; repeat necessary :test deps from deps.edn
32-
io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"}}
33-
:requires ([cognitect.test-runner])
34-
:task (do
35-
;; flag: sub-process should reload babashka.process
36-
(System/setProperty "babashka.process.test.reload" "true")
37-
;; flag: force use of bb even if natively compiled version of run-exec exists
38-
(System/setProperty "babashka.process.test.run-exec" "bb")
39-
;; run from babashka.process sources, not built-in babaska.process
40-
(require '[babashka.process] :reload)
41-
(apply cognitect.test-runner.-main *command-line-args*))}
74+
:task (apply shell shell-opts
75+
"bb -bb :test" *command-line-args*)}
4276

4377
test:jvm {:doc "Run jvm tests, optionally specify clj-version (ex. :clj-1.10 :clj-1.11(default) or :clj-all)"
4478
:requires ([clojure.string :as str]
@@ -72,7 +106,9 @@
72106
(doseq [alias aliases]
73107
(do
74108
(println (format "-[Running jvm tests for %s]-" alias))
75-
(apply clojure (str "-M:test" alias) "--namespace" "babashka.process-test" args))))}
109+
(apply clojure
110+
shell-opts
111+
(str "-M:test" alias) "--namespace" "babashka.process-test" args))))}
76112

77113
;; hidden CI support tasks
78114
-ci-install-jdk {:doc "Helper to download and install jdk under ~/tools on circleci"

deps.edn

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,10 @@
55
:clj-1.9 {:extra-deps {org.clojure/clojure {:mvn/version "1.9.0"}}}
66
:clj-1.10 {:extra-deps {org.clojure/clojure {:mvn/version "1.10.3"}}}
77
:clj-1.11 {:extra-deps {org.clojure/clojure {:mvn/version "1.11.3"}}}
8-
:clj-1.12 {:extra-deps {org.clojure/clojure {:mvn/version "1.12.0-beta1"}}}}}
8+
:clj-1.12 {:extra-deps {org.clojure/clojure {:mvn/version "1.12.0-beta1"}}}
9+
:nrepl/jvm {:extra-deps {nrepl/nrepl {:mvn/version "1.2.0"}
10+
cider/cider-nrepl {:mvn/version "0.49.1"}}
11+
:jvm-opts ["-XX:-OmitStackTraceInFastThrow"]
12+
:main-opts ["-m" "nrepl.cmdline"
13+
"--middleware" "[cider.nrepl/cider-middleware]"
14+
"-i"]}}}

src/babashka/process.cljc

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -178,17 +178,30 @@
178178
(str/includes? "windows")))
179179

180180
(defn- -program-resolver [{:keys [program dir]}]
181-
;; this should make life easier and not cause any bugs that weren't there previously
182-
;; on exception we just return the program as is
183-
(try
184-
(if (fs/relative? program)
185-
(if-let [f (fs/which (if dir
186-
(-> (fs/file dir program) fs/absolutize)
187-
program))]
188-
(str f)
189-
program)
190-
program)
191-
(catch Throwable _ program)))
181+
;; Used by default:
182+
;; - for all program resolution when running on Windows
183+
;; - by `exec` on all OSes
184+
;; Default ProcessBuilder behaviour on macOS/Linux does this work naturally
185+
;; and is therefore unneeded.
186+
;;
187+
;; ProcessBuilder program resolution on Windows does not entirely match
188+
;; behaviour of CMD Shell nor PowerShell.
189+
;; Here we adapt resolution to match PowerShell (with the exclusion of resolving
190+
;; .ps1 scripts) which follows the same strategy as macOS/Linux.
191+
(if-let [resolved (cond
192+
(fs/absolute? program)
193+
(fs/which program) ;; to resolve any extensions
194+
195+
(fs/parent program)
196+
(if dir
197+
;; we need to absolutize here to overcome a bug in Windows ProcessBuilder
198+
(some-> (fs/which (fs/file dir program)) fs/absolutize)
199+
(fs/which program))
200+
201+
:else
202+
(fs/which program))]
203+
(str resolved)
204+
(throw (ex-info (str "Cannot resolve program: " program) {}))))
192205

193206
(defn ^:no-doc default-program-resolver
194207
[{:keys [program] :as opts}]

test-native/src/babashka/test_native/run_exec.clj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@
4545
"call with a list of `args` for `babashka.process/exec`
4646
4747
for adhoc (under bash, Windows shell will have different escaping rules):
48-
bb exec-run.clj \"({:arg0 'new-arg0'} bb wd.clj :out foo :exit 3)\"
48+
bb run-exec.clj \"({:arg0 'new-arg0'} bb wd.clj :out foo :exit 3)\"
4949
5050
to avoid shell command escaping hell can also read args from edn file
51-
bb exec-run.clj --file some/path/here.edn"
51+
bb run-exec.clj --file some/path/here.edn"
5252
[& args]
5353
(let [exec-args (parse-args args)]
5454
(try
@@ -59,7 +59,7 @@
5959
(System/exit 42)
6060
(catch Exception ex
6161
;; an error occurred launching exec
62-
(println (pr-str (Throwable->map ex)))))))
62+
(println (pr-str {:bbp-test-run-exec-exception (Throwable->map ex)}))))))
6363

6464
;; support invocation from babashka when run as script
6565
(when (= *file* (System/getProperty "babashka.file"))

test-resources/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Babashka Process Test Resources
2+
3+
For test coverage, we need executables that emit their path and their current working directory.
4+
For example, on Windows:
5+
```
6+
> .\print-dirs.bat
7+
exepath: Z:\babashka\process\test-resources\print-dirs.bat
8+
workdir: Z:\babashka\process\test-resources
9+
> cd ..
10+
>test-resources\print-dirs
11+
exepath: Z:\babashka\process\test-resources\print-dirs.exe
12+
workdir: Z:\babashka\process
13+
```
14+
15+
This is straightforward for Linux/macOS `.sh`, Windows `.cmd` and Windows `.bat` variants, but for the Windows `.exe` variant, we need to create a binary.
16+
17+
## Generating a Windows .exe
18+
19+
It doesn't matter how `print-dirs.exe` is created; it won't need to be recreated often (we commit it to source control).
20+
21+
Since it generates relatively small binaries very quickly for any architecture, we've built a `print-dirs.exe` using Go.
22+
Source is in [print-dirs.go](print-dirs.go)
23+
24+
### Minimal Build
25+
Presuming you have [Go](https://go.dev/) installed, from this dir:
26+
27+
```shell
28+
GOOS=windows GOARCH=amd64 go build -o print-dirs.exe print-dirs.go
29+
```
30+
31+
For me, this generated a 1.9mb `print-dirs.exe`.
32+
33+
### Build a Smaller Exe
34+
Since this `.exe` is checked into version control, I opted to create a smaller binary by adding the following `-ldflags` to strip debug info:
35+
36+
```shell
37+
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o print-dirs.exe print-dirs.go
38+
```
39+
40+
For me, the generated `print-dirs.exe` is now 1.3mb.
41+
To further shrink down the binary, you can use [UPX](https://upx.github.io/) (which is also cross-platform):
42+
43+
```
44+
upx --ultra-brute print-dirs.exe
45+
```
46+
47+
And now `print-dirs.exe` is 457kb.
48+
49+
### Building With Docker
50+
If you don't want to install Go and UPX on your dev box but have docker installed, you can build from a docker image like so:
51+
52+
``` shell
53+
docker run --rm \
54+
-v "$PWD":/src \
55+
-w /src \
56+
devopsworks/golang-upx:latest \
57+
bash -c 'GOOS=windows GOARCH=amd64 \
58+
go build -ldflags "-s -w" -o print-dirs.exe print-dirs.go &&
59+
upx --ultra-brute print-dirs.exe'
60+
```
61+
62+
This is ultimately the command I ran to create our Windows .exe.
63+
64+
## What about Windows .com?
65+
66+
These days, `.com` executables are rare.
67+
68+
I could not find an easy way to create one.
69+
Our tests use `print-dirs.exe` copied to `print-dirs.com` to support `.com` test cases.

test-resources/print-dirs.bat

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@echo off
2+
echo exepath: %~dpnx0
3+
echo workdir: %cd%

test-resources/print-dirs.cmd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@echo off
2+
echo exepath: %~dpnx0
3+
echo workdir: %cd%

test-resources/print-dirs.exe

457 KB
Binary file not shown.

0 commit comments

Comments
 (0)