Dependencies & publishing
cljgo has no separate dependency manifest, and never reads deps.edn’s
:deps. Dependencies are
declared as code in your project’s build.cljgo (ADR 0021), resolved
by one resolver that feeds both execution legs — a dependency resolves
identically under cljgo run and cljgo build (ADRs 0052/0053).
Source roots: where namespaces are found
Section titled “Source roots: where namespaces are found”A project’s source roots default to src and test, whichever exist. So a
test tree beside the code works with no declaration:
src/app/core.cljg (ns app.core)test/app/core_test.cljg (ns app.core-test (:require [app.core :as c]))Both cljgo run and cljgo build resolve app.core from test/, because
src is a root.
Keeping your suite somewhere else? Say so once:
(defn build [b] (paths b ["src" "spec"]) …)Roots are appended after the requiring file’s own directory, never before,
so a sibling namespace still wins — and registered providers outrank every
root, so clojure.* can never be shadowed.
A project with no build.cljgo at all gets its roots from deps.edn’s
:paths instead (ADR 0119, v0.8.7) — :paths only, and only in that case; a
build.cljgo anywhere in the search wins absolutely. That is what lets a
dual-host .cljc library work on cljgo with no second project file. See
Dual-host .cljc projects.
Declaring a dependency
Section titled “Declaring a dependency”From examples/build-deps in the repo — an app depending on a local
library:
;; app/build.cljgo(defn build [b] (dep b "greetlib" {:path "../greetlib"}) (let [app (exe b {:name "app" :main "src/app/core.cljg"})] (install b app) (run b app)));; app/src/app/core.cljg — greet.core lives outside this tree;;; the dependency load path resolves it, in both legs.(ns app.core (:require [greet.core :as greet]))
(defn -main [& args] (println (greet/hello (if (seq args) (first args) "world"))))A git dependency looks the same, pinned by ref:
(dep b "greetlib" {:git "https://…" :ref "v1.2.0"}).
A Clojars/Maven coordinate is the third form (ADR 0095):
(dep b "org.clojure/tools.cli" {:mvn/version "1.1.230"})(dep b "medley" {:mvn/version "1.4.0"}) ; group == artifact(mvn-repo b "https://nexus.internal/repository/maven-public") ; optionalRepositories are shopped in order and the first one that answers serves the
artifact. The default list is Maven Central, then Clojars — the same order
tools.deps uses, so a coordinate published to both resolves to the same
artifact on cljgo as on the JVM (it was the other way round before v0.8.8).
(mvn-repo …) prepends to that list.
The dependency name is the coordinate (group/artifact; a single
segment means group == artifact), so a Maven dep and a git dep can never
collide by identity. The three coordinate kinds are mutually exclusive —
declaring two is an error, never a precedence rule.
Consuming Clojars libraries
Section titled “Consuming Clojars libraries”Resolution is pure Go: net/http + archive/zip + encoding/xml.
No JVM, no mvn, no Aether. cljgo walks the .pom graph itself
(breadth-first, first-wins, honouring <scope>, <optional> and
<exclusions>), downloads the jar, and extracts only the Clojure source
out of it — .class files and META-INF/ never land on your load path.
The honest scope
Section titled “The honest scope”cljgo consumes pure-Clojure libraries. Everything else fails loud, per namespace. That is the whole claim, and it is deliberately narrower than “consume the Clojure ecosystem”.
Spike s50 sampled seven real Clojars libraries and resolved them for real: 2 fully consumable, 4 partially, 1 not at all. The pattern is that the reachable set is utility and algorithm libraries — argument parsing, data helpers, templating — and not the Java-wrapping mainstream (HTTP clients, Jackson-backed JSON). Seven is a small sample and is quoted as one.
The gate is the NAMESPACE, not the library
Section titled “The gate is the NAMESPACE, not the library”One jar routinely mixes both: hiccup ships eight pure namespaces beside
two that (:import …) Java. A whole-library gate would be wrong in both
directions, so cljgo classifies per namespace:
- classification happens at resolve, and is recorded in
build.lock.ednunder:mvn/namespaces {:pure … :java …}— so the report is available offline, before a byte is fetched; - the failure happens at require, as
I4002, naming the namespace, the coordinate, the offending form, and how many other namespaces in the same library have no Java interop.
The library still resolves and locks. Only the use of a Java namespace fails.
What “no Java interop” measures — and what it does not
Section titled “What “no Java interop” measures — and what it does not”The resolve line reads
cljgo deps: medley/medley 1.4.0 — 1 namespace(s) with no Java interopand that sentence is the whole claim. Classification is a read-time
check: cljgo’s reader read the file (with :cljgo reader conditionals
resolved) and found no Java interop in what survived. It does not
compile the namespace, so it cannot promise the namespace compiles — that
would need core plus every one of the library’s own requires already
loaded, and any gap in cljgo’s analyzer would then be printed as a fact
about somebody else’s library.
The line used to say “N namespace(s) usable”, which claimed more than was
measured — and a library could then fail to build right after being called
usable. If a namespace that passed the check fails anyway, the require
raises G5020, which names the measurement that
passed, what failed, and that the gap is cljgo’s, not the library’s.
Reader conditionals
Section titled “Reader conditionals”A .cljc is read with cljgo’s platform feature :cljgo (plus
:default). cljgo does not claim :clj — that is the JVM’s
feature, and claiming it would pull in the very branch a portable
library fenced away from non-JVM hosts. This is what makes a library
like medley honestly consumable: its java.util calls live in
#?(:clj …) branches cljgo never reads, so they are not in the forms at
all.
The flip side: a .cljc whose real top-level body is :clj-only gives
cljgo nothing loadable, and fails loud with
R1012 rather than installing a namespace with
no vars.
What name-errors instead of guessing
Section titled “What name-errors instead of guessing”The POM subset is small on purpose. When a POM needs something outside
it, cljgo names the feature and stops (G5011) rather than guess —
an uninterpolated or absent version is a wrong version:
${property} interpolation, <dependencyManagement> version supply,
<parent> inheritance, version ranges, -SNAPSHOT, <profiles>,
<classifier>, and non-jar packaging. A disagreeing version between two
requirers is G5013, naming both, resolved with accept-version — no
MVS, no silent newest-wins.
org.clojure/clojure, spec.alpha and core.specs.alpha are pruned
from every graph (cljgo is the Clojure implementation; its
clojure.core is embedded), and the prune is reported, not silent.
Third-party Go modules are one line, not a binding:
(go-require app "github.com/gorilla/websocket" "v1.5.3") — cljgo
synthesizes the generated go.mod and the emitter links the real module
(see the interop guide).
The lockfile
Section titled “The lockfile”The first cljgo build writes build.lock.edn next to
build.cljgo. Commit it. Per dependency it records identity
(:git/url, :git/ref, :git/sha), a merkle :tree/hash verified on
every read (a git SHA alone is not a content hash), the dep’s source
:paths, its transitive :requires, and whether it is :pure?.
Changing a dependency, and freezing one
Section titled “Changing a dependency, and freezing one”Edit a version in build.cljgo and the next cljgo build re-pins it
for you. The lock records a hash of the declared set, so cljgo can see
that the manifest moved (ADR 0112).
The re-pin is minimal: only the declarations that actually changed,
and whatever is reachable only from them, are resolved again. Every
other pin is kept exactly as it was — so bumping one library cannot
quietly drift every unrelated transitive to whatever is newest today.
Cosmetic edits — a comment, a reformat, renaming your exe — declare
nothing, so they re-resolve nothing.
For CI, that default is wrong, and there is a flag for it:
cljgo build --locked # or CLJGO_LOCKED=1Frozen mode turns a stale lock into an error (G5021) and never
rewrites it. The case it exists for is a merge — one branch’s
build.cljgo landing beside another branch’s build.lock.edn — which
would otherwise build green against a dependency graph nobody reviewed.
This is not the same as --offline: you can be perfectly online and
still want the lock to be the authority.
Once locked, cljgo run resolves the same dependencies the same way —
one resolver, both legs, so the interpreter and the binary can never see
different library code.
Some deliberate properties of the resolver (ADR 0052):
- Global content-verified cache under
$XDG_CACHE_HOME/cljgo(or~/.cache/cljgo; override with$CLJGO_CACHE). Entries are immutable read-only trees, so removal is a verb:cljgo cache clean. - Version conflicts are a hard error, not silent minimal-version selection — the error names both requirers and both versions.
- Transitive deps come from the lock as data. A dependency’s
build.cljgois never executed during resolution — no arbitrary code runs just to discover the graph. clojure.*cannot be shadowed by a dependency root — a deliberate, recorded divergence from the JVM classpath.- A project-local
vendor/<name>/directory overrides the cache under the same lock hash, for air-gapped or audited builds.
Source files may be .clj, .cljc, .cljg, or .cljgo; the build file
is probed as build.cljgo > build.cljg,
most-specific-first (ADR 0055).
Publishing: one library, both ecosystems
Section titled “Publishing: one library, both ecosystems”A cljgo project publishes from the same build.cljgo that builds it —
no second manifest (ADR 0054). The project declares a library artifact
((lib b …)), and the target is chosen at the command line:
cljgo publish go # a go-gettable Go modulecljgo publish clojars # pure Clojure source for JVM-Clojure consumersFlags (all optional): -o dir output directory (default
./publish/<target>), -name lib which library artifact, -module
override the module path/coordinate, -runtime cljgo source tree for
the generated go.mod (publish go).
Purity decides which targets a library qualifies for, checked at publish time over the whole transitive required surface:
| the library uses… | publish go |
publish clojars |
|---|---|---|
| pure Clojure only | yes | yes |
require-go (Go interop) |
yes | refused, with file:line |
A pure-Clojure library is the only artifact that reaches both worlds. The
moment a reachable namespace uses Go interop, it is Go-side only — and
publish clojars names the offending file and line instead of shipping a
broken download. Go developers then just go get the module; JVM-Clojure
developers consume the source via a git coordinate in their deps.edn.
Honest edges (implemented vs deferred)
Section titled “Honest edges (implemented vs deferred)”publish gowrappers currently exposeanysignatures; typed signatures from type hints are a tracked follow-up, as is wiring a library’s own third-partygo-requireinto the published module.publish clojarsis git-coordinate distribution today — the actual Clojars coordinate/source-jar upload step is deferred.- Consuming from Clojars means: cljgo consumes pure-Clojure libraries; everything else fails loud, per namespace. It is not “consume the Clojure ecosystem”. See Consuming Clojars libraries below for what that reaches, measured rather than claimed.
c-shared/c-archivelibrary targets (ADR 0013) are not built yet.
To ship an executable instead, see Compile & ship binaries.
Questions or feedback on this page? Comment below with your GitHub account — comments are public and live in the project's GitHub Discussions.