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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions magic-compiler/dll-sources.edn
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}
7 changes: 1 addition & 6 deletions nostrand/Nostrand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
140 changes: 96 additions & 44 deletions nostrand/nostrand/core.clj
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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]
Expand All @@ -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))]
Expand All @@ -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))))))
33 changes: 18 additions & 15 deletions nostrand/nostrand/deps/basis.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand All @@ -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)))}))
10 changes: 5 additions & 5 deletions nostrand/nostrand/tasks.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))))))

Expand All @@ -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"])
Expand Down Expand Up @@ -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*)
Expand Down Expand Up @@ -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]
Expand Down
Binary file modified nostrand/references/nostrand.core.clj.dll
Binary file not shown.
Binary file modified nostrand/references/nostrand.deps.basis.clj.dll
Binary file not shown.
Binary file modified nostrand/references/nostrand.tasks.clj.dll
Binary file not shown.
Loading