diff --git a/CHANGELOG.md b/CHANGELOG.md index 4845859f..df2edc2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Nostrand - `nos` ships its own eight namespaces as committed `nostrand.*.clj.dll` instead of compiling them in memory at every startup, so a short invocation starts roughly twice as fast. `bb build-runtime` recompiles them - [#183](https://github.com/flybot-sg/magic/issues/183). - `*load-paths*` holds each root once, and `set-load-path` drops roots it no longer names - [#182](https://github.com/flybot-sg/magic/issues/182). +- Load-path and assembly roots pin to the directory they were added from, and `establish-project` takes the project root explicitly instead of reading the process directory; it owns both searches, so a second call switches projects rather than searching the first one first - [#187](https://github.com/flybot-sg/magic/issues/187). ## v0.13.0 - 2026-09-09 diff --git a/magic-compiler/dll-sources.edn b/magic-compiler/dll-sources.edn index bb751606..bdbec139 100644 --- a/magic-compiler/dll-sources.edn +++ b/magic-compiler/dll-sources.edn @@ -68,11 +68,11 @@ magic.spells.lift_keywords {:source "magic-compiler/src/magic/spells/lift_keywords.clj", :sha256 "907dc9a166b32a95d237440b5875a186de05e7786390a8fba095382611eaf75e"} magic.spells.lift_vars {:source "magic-compiler/src/magic/spells/lift_vars.clj", :sha256 "1dafcdadb721c083f867e6935b9cb9ea3bdc2e2e7a8f07cd3c0daae9f4293ead"} magic.util {:source "magic-compiler/src/magic/util.clj", :sha256 "62d16cbc45160f486776f6c0c240bf6762846bbcbb59cf1cd45d409defe9c7d7"} - nostrand.core {:source "nostrand/nostrand/core.clj", :sha256 "908042526658ea1a8a20684d1e2b8c9a72336522273973ac7b3eab71142f4c1b"} - nostrand.deps.basis {:source "nostrand/nostrand/deps/basis.clj", :sha256 "0c66338b1923de0298a19e5d7202d450653949f22983a80b9bfa62199b143e4e"} + nostrand.core {:source "nostrand/nostrand/core.clj", :sha256 "457214655318fb5f8eede402ffe5828ae4fa8da0214b5fedb2296d271c96b971"} + nostrand.deps.basis {:source "nostrand/nostrand/deps/basis.clj", :sha256 "5e7181e2ddae6edde623e51aa3fa68d60e17dae7285929448b06a02c7eedccd8"} nostrand.deps.git {:source "nostrand/nostrand/deps/git.clj", :sha256 "f10234b211298a2c5b79b9e067ecb54227b3a1df11938e7e264a979d5326f5e7"} nostrand.deps.nuget {:source "nostrand/nostrand/deps/nuget.clj", :sha256 "589591b8b9fde3af2a7d9e6f0f5e3195a6249833d4f992f80dba90f4507959a0"} nostrand.deps.shell {:source "nostrand/nostrand/deps/shell.clj", :sha256 "623bd8876470a09fcbe652d7de51152e6ecf3b89df38b03bbd7c867137212271"} nostrand.deps.submodules {:source "nostrand/nostrand/deps/submodules.clj", :sha256 "adf03193ee08b2a44854f19e049fa0892f1cad1fabfb419ad88330c8ce6b7684"} nostrand.repl {:source "nostrand/nostrand/repl.clj", :sha256 "05755d291b897f6ffe62587d034d534c71f00b9c504e8081538a1e819aecb4de"} - nostrand.tasks {:source "nostrand/nostrand/tasks.clj", :sha256 "1fec0b7038e4c57801747cd73a08553d3de01b71fc427d7cbd8766f5ce8a4616"}} + nostrand.tasks {:source "nostrand/nostrand/tasks.clj", :sha256 "b8348ef34e5b16bbb823a66108ff8b321b56d36656cba37650a1e77062a5f7f8"}} diff --git a/nostrand/Nostrand.cs b/nostrand/Nostrand.cs index 5b9919fe..4b826efd 100644 --- a/nostrand/Nostrand.cs +++ b/nostrand/Nostrand.cs @@ -168,12 +168,7 @@ public static void Main(string[] args) { AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolver.Resolve; - RT.var("nostrand.core", "load-path").invoke(Directory.GetCurrentDirectory()); - - if (File.Exists("deps.edn") || File.Exists("deps-clr.edn")) - { - RT.var("nostrand.core", "establish-deps-edn").invoke(); - } + RT.var("nostrand.core", "establish-project").invoke(Directory.GetCurrentDirectory()); RT.PostBootstrapInit(); diff --git a/nostrand/nostrand/core.clj b/nostrand/nostrand/core.clj index e85eaffe..53a44323 100644 --- a/nostrand/nostrand/core.clj +++ b/nostrand/nostrand/core.clj @@ -1,27 +1,61 @@ (ns - ^{:author "Ramsey Nasser" - :doc "Core nostrand API containing load path, assemblies, and dependency functions."} - nostrand.core + ^{:author "Ramsey Nasser" + :doc "Core nostrand API containing load path, assemblies, and dependency functions."} + nostrand.core (:require [clojure.string :as string] [nostrand.deps.basis :as basis] [nostrand.deps.submodules :as submodules]) (:import [System.IO Directory Path File])) -(def -assembly-path - (atom (string/split (or (Environment/GetEnvironmentVariable "MONO_PATH") ".") - (re-pattern (str Path/PathSeparator))))) +(defn- absolute-path + "Resolve the absolute path against the current cwd, in case the cwd changes later." + [path] + (when (string/blank? (str path)) + (throw (ex-info "Load path entry is blank" {:path path}))) + (let [full (Path/GetFullPath (str path))] + (if (and (> (count full) 1) + (string/ends-with? full (str Path/DirectorySeparatorChar))) ; remove trailing separator + (subs full 0 (dec (count full))) + full))) -(def -load-path - (atom (string/split (or (Environment/GetEnvironmentVariable "CLOJURE_LOAD_PATH") ".") - (re-pattern (str Path/PathSeparator))))) +(defn- path-list + "Absolute entries of a PathSeparator-joined list. Blank entries are dropped + rather than rejected: they come from a stray separator, not a named path." + [s] + (into [] (comp (remove string/blank?) (map absolute-path)) + (string/split (or s "") (re-pattern (str Path/PathSeparator))))) + +;; No cwd default: the cwd here is the host's; establish-project seeds the root. +(def ^:private -env-assembly-path + (path-list (Environment/GetEnvironmentVariable "MONO_PATH"))) + +(def ^:private -env-load-path + (path-list (Environment/GetEnvironmentVariable "CLOJURE_LOAD_PATH"))) + +(def -assembly-path (atom -env-assembly-path)) + +(def -load-path (atom -env-load-path)) (defonce ^{:private true :doc "*load-paths* as the runtime left it, set when this namespace is first loaded, so a host must load nostrand.core before adding any project root."} -base-load-paths (vec *load-paths*)) -(defn- absolute-load-path [] - (into [] (comp (map #(Path/GetFullPath %)) (distinct)) @-load-path)) +(def ^:private -project-root + (atom nil)) + +(defn project-root + "Directory the project deps file and its relative :paths resolve against." + [] + (or @-project-root + (throw (ex-info "No project root: establish-project has not run" {})))) + +(defn project-deps-file + [] + (basis/project-deps-file (project-root))) + +(defn- load-path-roots [] + (into [] (distinct) @-load-path)) (defn resolve-assembly-load [asm] (let [candidates (for [prefix @-assembly-path @@ -34,26 +68,31 @@ (assembly-load-from full-asm-path)))) (defn update-load-path [] - (let [abs-paths (absolute-load-path)] + (let [roots (load-path-roots)] ;; CLOJURE_LOAD_PATH gets absolute roots (like *load-paths*), so a loader - ;; scanning it finds files from any cwd, matching ClojureCLR. + ;; scanning it finds files from any cwd, matching ClojureCLR. Prefer nil + ;; over "", to unset the variable when there are no paths. (Environment/SetEnvironmentVariable - "CLOJURE_LOAD_PATH" - (string/join Path/PathSeparator abs-paths)) + "CLOJURE_LOAD_PATH" + (when (seq roots) (string/join Path/PathSeparator roots))) (alter-var-root #'*load-paths* (constantly - (into abs-paths (remove (set abs-paths)) -base-load-paths))))) + (into roots (remove (set roots)) -base-load-paths))))) (defn set-load-path [val] - (reset! -load-path val) + (reset! -load-path (mapv absolute-path val)) (update-load-path)) (defn add-load-path [path] - (swap! -load-path conj path) + (swap! -load-path conj (absolute-path path)) (update-load-path)) (defn add-assembly-path [path] - (swap! -assembly-path conj path)) + (swap! -assembly-path conj (absolute-path path))) + +(defn set-assembly-path + [val] + (reset! -assembly-path (mapv absolute-path val))) (defn load-path [& paths] (doseq [p paths] @@ -80,15 +119,14 @@ (defn loaded-assembly-files "The files of the assemblies this process loaded from under a source path. - The current directory sits on the load path to resolve task files, not as a + The project root sits on the load path to resolve task files, not as a source path, so matching it would take in every assembly beneath it." [] - (let [cwd (dir-prefix (Path/GetFullPath ".")) - prefixes (->> (absolute-load-path) - distinct + (let [root (dir-prefix (project-root)) + prefixes (->> (load-path-roots) (filter #(Directory/Exists %)) (map dir-prefix) - (remove #(= cwd %)))] + (remove #(= root %)))] (for [asm (.GetAssemblies AppDomain/CurrentDomain) :let [file (assembly-file asm)] :when (and file (under-any? prefixes file))] @@ -103,23 +141,37 @@ `(reference* ~(mapv str asms))) (defn establish-deps-edn - "Resolve a deps.edn (with the given aliases) and put every resolved - source path on the load path. Returns the basis. The 0-arity is the - boot entry point: it activates the aliases under the :nos/aliases key, - and when :nos/submodule-paths is present also adds the source paths of - the vendored git submodules (its value is a path prefix to restrict to, - or true for every submodule)." - ([] - (let [deps-file (basis/project-deps-file) - deps-edn (basis/read-project-deps deps-file) - b (establish-deps-edn deps-file (:nos/aliases deps-edn []))] - (when-let [root (:nos/submodule-paths deps-edn)] - (when (File/Exists ".gitmodules") - (apply load-path - (submodules/submodule-paths (slurp ".gitmodules") - (when (string? root) root))))) - b)) - ([deps-file aliases] - (let [{:keys [classpath-paths] :as b} (basis/create-basis deps-file aliases)] - (apply load-path classpath-paths) - b))) + "Resolve deps-file (with the given aliases) and put every resolved source + path on the load path. Returns the basis." + [deps-file aliases] + (let [{:keys [classpath-paths] :as b} (basis/create-basis deps-file aliases)] + (apply load-path classpath-paths) + b)) + +(defn establish-project + "Make root the project root and reset both searches to the env-var roots + plus root, replacing any previous project, then add the source paths from + root's deps file. + extra-roots are source roots searched after root; they do not join the + assembly search. Returns the basis, or nil when root has no deps file." + ([root] (establish-project root nil)) + ([root extra-roots] + (let [root (absolute-path root)] + (when-not (Directory/Exists root) + (throw (ex-info (str "Project root is not a directory: " root) {:root root}))) + (reset! -project-root root) + (set-load-path (into (conj -env-load-path root) extra-roots)) + (set-assembly-path (conj -env-assembly-path root)) + (let [deps-file (basis/project-deps-file root)] + (when (File/Exists deps-file) + (let [deps-edn (basis/read-project-deps deps-file) + b (establish-deps-edn deps-file (:nos/aliases deps-edn [])) + ;; A path prefix to restrict the submodules to, or true for all. + sub (:nos/submodule-paths deps-edn) + modules (Path/Combine root ".gitmodules")] + (when (and sub (File/Exists modules)) + (apply load-path + (map #(Path/Combine root %) + (submodules/submodule-paths (slurp modules) + (when (string? sub) sub))))) + b)))))) diff --git a/nostrand/nostrand/deps/basis.clj b/nostrand/nostrand/deps/basis.clj index 88b8d0bb..b14dc6fe 100644 --- a/nostrand/nostrand/deps/basis.clj +++ b/nostrand/nostrand/deps/basis.clj @@ -129,10 +129,10 @@ full sha it abbreviates are treated as equal). A coord's :exclusions prune those libs from its subtree, and an entry in overrides replaces a lib's coord wherever it is encountered (a JVM->CLR fork swap), without seeding a root - dependency for libs absent from the tree. Returns - lib -> {:coord :resolved-sha :paths}." - [cache deps overrides] - (loop [queue (vec (for [[lib coord] deps] [lib coord "." #{}])) + dependency for libs absent from the tree. Relative coords resolve against base, + a dependency's against its checkout. Returns lib -> {:coord :resolved-sha :paths}." + [cache base deps overrides] + (loop [queue (vec (for [[lib coord] deps] [lib coord base #{}])) out {} skipped []] (if-let [[lib coord0 base excluded] (first queue)] @@ -183,10 +183,11 @@ (str (Environment/GetEnvironmentVariable "HOME") "/.nostrand/gitlibs"))) (defn project-deps-file - "deps-clr.edn if present, else deps.edn. Matches cljr, which reads - deps-clr.edn in place of deps.edn." - [] - (if (File/Exists "deps-clr.edn") "deps-clr.edn" "deps.edn")) + "Absolute path of the deps file in dir: deps-clr.edn if present, else + deps.edn, as cljr prefers." + [dir] + (let [clr (Path/Combine dir "deps-clr.edn")] + (if (File/Exists clr) clr (Path/Combine dir "deps.edn")))) (defn read-project-deps "Parse the project deps file, naming it in the error when it is missing @@ -203,10 +204,12 @@ (defn create-basis "Read deps-file, fold in the selected aliases, resolve transitively, and return {:paths :libs :classpath-paths}." - ([] (create-basis (project-deps-file) [])) - ([deps-file aliases] - (let [{:keys [paths deps overrides]} (merge-aliases (read-project-deps deps-file) aliases) - libs (resolve-deps (cache-root) deps overrides)] - {:paths paths - :libs libs - :classpath-paths (concat paths (mapcat :paths (vals libs)))}))) + [deps-file aliases] + (let [base (Path/GetDirectoryName (Path/GetFullPath deps-file)) + {:keys [paths deps overrides]} (merge-aliases (read-project-deps deps-file) aliases) + ;; :local/root resolves against base too, in resolve-deps. + paths (mapv #(resolve-root base %) paths) + libs (resolve-deps (cache-root) base deps overrides)] + {:paths paths + :libs libs + :classpath-paths (concat paths (mapcat :paths (vals libs)))})) diff --git a/nostrand/nostrand/tasks.clj b/nostrand/nostrand/tasks.clj index 7f997c4b..b046cd37 100644 --- a/nostrand/nostrand/tasks.clj +++ b/nostrand/nostrand/tasks.clj @@ -99,7 +99,7 @@ Usage: nos print-basis ; base :deps only nos print-basis :clr :test ; with aliases" [& aliases] - (let [{:keys [paths libs classpath-paths]} (basis/create-basis (basis/project-deps-file) (vec aliases))] + (let [{:keys [paths libs classpath-paths]} (basis/create-basis (nos/project-deps-file) (vec aliases))] (pprint/pprint {:aliases (vec aliases) :paths paths @@ -189,7 +189,7 @@ compile or test instead of hand-listing it. Read-only; does not touch the load path. (tasks/project-namespaces [:test])" - ([aliases] (project-namespaces (basis/project-deps-file) aliases)) + ([aliases] (project-namespaces (nos/project-deps-file) aliases)) ([deps-file aliases] (paths-namespaces (:paths (basis/create-basis deps-file (vec aliases)))))) @@ -200,7 +200,7 @@ namespaces from either source (e.g. test-dir tooling that must not load)." [namespaces aliases exclude basis] (->> (or namespaces - (paths-namespaces (:paths (or basis (basis/create-basis (basis/project-deps-file) (vec aliases)))))) + (paths-namespaces (:paths (or basis (basis/create-basis (nos/project-deps-file) (vec aliases)))))) (remove (set exclude)))) (def ^:private clj-assembly-suffixes [".clj.dll" ".cljc.dll" ".cljr.dll"]) @@ -275,7 +275,7 @@ (defn build [] (tasks/compile-project :aliases [:clr]))" [& {:keys [namespaces exclude aliases out csharp-out clean? flags] :or {out "build" clean? false flags production-flags}}] - (let [basis (when (seq aliases) (nos/establish-deps-edn (basis/project-deps-file) aliases)) + (let [basis (when (seq aliases) (nos/establish-deps-edn (nos/project-deps-file) aliases)) nses (task-namespaces namespaces aliases exclude basis)] (with-bindings (assoc flags #'*compile-path* out) (println "Compiling into DLL dir:" *compile-path*) @@ -315,7 +315,7 @@ (defn run-tests [] (tasks/run-clojure-tests :aliases [:clr :test]))" [& {:keys [namespaces exclude re aliases flags exit? exclude-vars] :or {flags production-flags exit? true}}] - (let [basis (when (seq aliases) (nos/establish-deps-edn (basis/project-deps-file) aliases)) + (let [basis (when (seq aliases) (nos/establish-deps-edn (nos/project-deps-file) aliases)) nses (task-namespaces namespaces aliases exclude basis)] (with-bindings flags (doseq [ns nses] diff --git a/nostrand/references/nostrand.core.clj.dll b/nostrand/references/nostrand.core.clj.dll index b6c95f3a..42adb865 100755 Binary files a/nostrand/references/nostrand.core.clj.dll and b/nostrand/references/nostrand.core.clj.dll differ diff --git a/nostrand/references/nostrand.deps.basis.clj.dll b/nostrand/references/nostrand.deps.basis.clj.dll index 92748e08..f429d02d 100755 Binary files a/nostrand/references/nostrand.deps.basis.clj.dll and b/nostrand/references/nostrand.deps.basis.clj.dll differ diff --git a/nostrand/references/nostrand.tasks.clj.dll b/nostrand/references/nostrand.tasks.clj.dll index ea7b4819..5011e180 100755 Binary files a/nostrand/references/nostrand.tasks.clj.dll and b/nostrand/references/nostrand.tasks.clj.dll differ