fmjrey/project {:git/tag "TAG" :git/sha "SHA"}
Utility library to capture project info and retrieve it at runtime.
WORK IN PROGRESS
This library is in a pre-alpha state meaning the API is very likely to change. Although the remit of capturing and retrieving project info appears simple, the variety of ways in which clojure, and its derivatives beyond the JVM, handle dependencies, their distribution, and artifact creation introduces some complexity, and the need to offer flexibility and openness.
The difficulty with a hosted language is that the ecosystem it is hosted on most likely already has tooling and formats for handling dependencies and project info. There is however some value in having a uniform access to these, even if the data comes from host-specific mechanisms. For example in polyglot projects it may be useful to have access to all dependencies with version and license info to analyze potential security and licensing issues.
Below are the goals for this library:
- Initial focus is on capturing project info in
deps.ednwhile allowing for other file and format because other host languages may have well defined ways to handle the same concern. Going beyond that initial focus largely depends on interest and contributions (see the Customization and Feedback to/from the core team sections). - This library proposes some flexibility in the logic for retrieving project
info at runtime (see Options and Customization
sections). It also provides the necessary logic and tools to prepare that
info during development and build time. On the JVM this consists in copying
the
deps.ednfile into a resource directory (see Use within build.clj). - There should be a clear distinction between the runtime and build time logic, so as to not force a project to bring dependencies at runtime that are only relevant during development (see Usage section).
- Use the same simple API for clojure on the JVM and its derivatives hosted
elsewhere. The function
infoand macroproject-infoare proposed to be that simple entry point. The reason for having a macro is because there may be logic to be run where the call is made (e.g. getting the caller classloader). Additional functions are there for customization, diagnosis, and experimentation (see Usage section). - It should be possible for a library existing as a dependency in a dependent project to retrieve its own project info as well as the info of the owning project. To that end the use of a project/library identifier is essential.
- This library does not dictate what a project info is made of. It however
assumes it is a single map with a mandatory project/library identifier
under the
:idkey. The use of similar keys across languages is encouraged where it makes sense, see:project/infoalias entries. - This library should depend on a limited set of functionality and dependencies
and ideally have most of its logic in CLJC. At present it is written in
.cljfiles because its initial focus makes it rely on tools.deps.edn.
As the clojure CLI and build tools mature and become prevalent, the feature gap with the previous leiningen tool is diminishing. Capturing project metadata such as name and version remains however unhandled, and there aren't many places where it can be.
Tools.build offers APIs that help
with versioning, and thus may seem like a good place to start. Except projects
do not necessarily need a build file, or have a it named build.clj.
Also build.clj isn't so much about data, and more about logic, though it most
likely needs project data to carry its work.
Then there is deps.edn which mostly deals with dependencies and does not say
much about the dependent side (e.g. :paths). It could say more, the verb
to depend is transitive and requires a dependent subject and one or more
direct dependency objects.
A major hurdle at present is that neither are guaranteed to be present
in a release artifact, certainly not build.clj as many projects don't need
one, or even are required to name it so. The trend to use direct git coordinates
may provide a way to access deps.edn but for now it does not. Moreover jar
artifacts do not usually contain the deps.edn unless it's copied in a
resource directory at build time.
Still, having a well defined place to capture project metadata helps
tremendously different uses cases, such as discovery and tooling.
The recent creation of
tools.deps.edn,
JIRA TDEPS-277
and TDEPS-278,
seem to point towards a future where a project deps.edn might be made
available more systematically regardless of the situation.
In terms of design here are some important considerations:
-
Q: Where in
deps.ednshould project info be captured?A: For now adding data within an alias is the recommended way to add user keys in
deps.edn. The core team prefers to reserve top level keys for future evolution and cannot guarantee future access to custom keys, see the official documentation about this here. That being said, the most logical place for project data would be at the same level as:pathsand:depssince these already capture data about a project. Even with an alias, there is no guarantee its name won't conflict with other user aliases. Therefore this library uses the qualified alias:project/info, and allows for it to be changed via some option. -
Q: How is this data going to be merged with root and user
deps.edn?A: Project data shouldn't be defined elsewhere than in the project root
deps.edn. There are two different merge strategies being applied, the first one being the most constraining for a:project/infoalias:- First step: combine all the different root, user, project, and extra
deps.edninto a single one usingclojure.core/merge. In other words the last entry (the most specific one on the left) replaces the previous ones (the most generic one on the right), which means the last alias wins, as documented here. - Second step: use that combined
deps.ednto reduce the aliases supplied to the CLI into a runtime basis so as to build a classpath, using per-key merge rules, and as documented here.
- First step: combine all the different root, user, project, and extra
-
Q: Can the runtime basis be used to retrieve project data?
A: Yes but only to retrieve the dependent project data when launched via the clojure CLI. If a library used as a dependency would like to report on its own data (not on the dependent project) it needs to access its own
deps.edn, which is why this file is copied as a resource because most logic for building a jar do not include it by default. Also there may be cases where the clojure CLI isn't used to launch a project, which is likely to happen with clojure dialects that still capture data indeps.ednbut use other means to launch a project or script. -
Q: If both
deps.ednin project root and a copy in a resource directory are available, which one is loaded?A: Load from project root first, and if not found try as a resource. See the Searched locations section.
-
Q: How to ensure a library is loading its own
deps.ednand not the one from another library or even from the dependent application?A: Copy the
deps.ednfile into thedeps/<group-id>/<artifact-id>/resource directory. Searchdeps.ednfirst in project root then in the resource directory, accepting the first that matches a givengroupId/artifactId. -
Q: Can a source dependency (git or local) access its own project
deps.ednfrom within its code?A: TBD. A dependency provided as a jar is not guaranteed to have its
deps.ednpackaged along the code, whereas its own resources certainly are. Source dependencies however should only have adeps.ednin project root since the resource copy is not supposed to be checked in. Moreover accessing resources is very much dependent on the classloading strategy which may vary from one context to the other (e.g. uberjar, web or app server, etc.). Nevertheless the expectation for source dependencies is that there should always be a way to loaddeps.ednfrom project root, though some experimentation is needed to validate the most reliable way to do that. See the Applicability for each location for some attempts to describe what's to be expected (feedback welcomed).
This library is therefore proposing to use deps.edn as the place where
project data is captured into a new :project/info alias. It also provides a
way to retrieve it at runtime by copying deps.edn into a resource directory.
The :project/info alias in deps.edn contains a single map of data about
the project such as version, name, license, etc. Only the version in a project
root should be edited directly and version controlled. The copy that is made
in a resource directory should not be edited and preferably not checked in.
In deps.edn, project related data about the dependent side should be placed
in a map under the :project/info alias, and may contain the following entries:
:id: a qualified symbol identifying the project, as expected under:libby write-pom and similar to the name defined withdefprojectin leiningen.:version-string: the full version identifier as a string, typically computed from the:versionand:sourceentries:description: a short description of the project:url: the home URL of the project:license: a map, or vector of maps in case of multiple licenses, containing license information with the following keys::id: the SPDX identifier of the license, if any:name: the name of the license:url: the URL of the license
:version: map containing internal version information, typically::major: the major version number as an integer:minor: the minor version number as an integer:patch: the patch version number as an integer
:source: map containing coordinates for the project source, including::url: the URL where to find the source code, such as a git repository:rev: the revision reference for this release, which in the case of git could be anything thatgit rev-parsecan understand such as tags, abbreviated or full commit hashes, or even branch names.
Of these keys only :id is mandatory, though others are strongly recommended as
they help tooling and discovery. In particular :version-string is highly
recommended, so that it can be discovered at runtime and used for logging,
greeting and diagnostic printing, etc.
Entries in the :version map are not normative, only suggested. The intent is
to capture the most significant data to compute a full version so it remains
consistent, editable, and does not always require parsing to extract useful
information.
Entries in the :source map are not normative either. While for dependencies
there is a need to be very precise on how to fetch source code, the goal here
is not to fetch the code, but to be informative enough for humans and tooling
such as versioning. The idea is to not be specific to any VCS so keys should
be considered generic, e.g. :rev refers to the concept of revision that is
applicable to most VCS. Also in the unlikely but possible situation where the
source isn't provided as a repository, the :url could point to a downloadable
archive or a page where it can be found. The alternative to generic keys is to
use specific ones like those for git coordinates in :deps. You are free to do
that if that makes more sense for your use case.
Projects may add additional entries as needed. For example the versioning logic could rely on such additional key:
:version-pattern: theformatpattern for creating the:version-stringalong with an argument list referencing other keys in the:project/infomap using the syntax["format-pattern" :single-key [:submap :key]...], e.g.:["v\\d+\\.\\d+\\.\\d+-\\s" [:version :major] [:version :minor] [:source :rev-count] [:source :sha]]
The above is not suggesting to store SHA and revision count inside deps.edn.
In fact values that quickly change over time such as these are discouraged in
:project/infounless you have automation to always keep them up to date.
However one could imagine the project map to be augmented at runtime with these
time-dependent entries in order to provide them to the next build step.
Finally, in the rare case where a different alias name needs to be used to
capture project info, the :fmjrey.project/alias option can be set to the alias
keyword where to find project data, which defaults to :project/info.
This library can be used in 3 different ways:
- at runtime as a library in order to read project info from
deps.ednwherever it may be - within your project
build.cljor equivalent - as a clojure CLI command with the -T or -X options
A single namespace fmjrey.project contains the API for runtime, build, and CLI
usage. However to not load the clojure build API when using it at runtime,
io.github.clojure/tools.build is not declared as a dependency by this project,
and therefore must be declared as a dependency in yours, as illustrated
by the sample deps.edn below:
{:deps {fmjrey/project {:git/tag "TAG" :git/sha "SHA"}} ;; runtime use
:aliases {
;; easier to find as the first alias
:project/info {:id: my.app/name
:name "my app name"
:license {:id "EPL-2.0"
:name "Eclipse Public License 2.0"
:url "https://www.eclipse.org/legal/epl-2.0"}}
;; CLI use with -X
:project {:deps {;; tools.build should be declared here to use copy-deps
io.github.clojure/tools.build {:mvn/version "0.10.12"}
fmjrey/project {:git/tag "TAG" :git/sha "SHA"}}
:exec-args {:fmjrey.project/verbose true} ; otherwise no printing
:ns-default fmjrey.project}
;; build and task use with -T
:build {:deps {;; tools.build must be declared here
io.github.clojure/tools.build {:mvn/version "0.10.12"}
fmjrey/project {:git/tag "TAG" :git/sha "SHA"}}
:ns-default build}}}Note how the above deps.edn defines 2 aliases that both declare
io.github.clojure/tools.build as a dependency:
:project: for easier CLI use as it defaults to thefmjrey.projectnamespace and adds the verbose option (otherwise nothing is printed),:build: for use inbuild.clj.
Having access to project data can be useful for printing or logging information
at runtime, e.g. a header string containing name and version upon startup.
Most situations will only need the project-info macro or info function
to retrieve a project info. Additional functions are provided for more
customized behavior and experimentation.
In all cases the require entry should be as follows for runtime use:
[fmjrey.project :as project]The easiest way to load project data is to use the macro project/project-info
or the project/info function in order to define a var to capture the
:project/info alias map, .e.g:
(def app-info (project/project-info)) ;; the macro sets the classloader optionor
(def app-info (project/info))The project-info macro and info function have an identical signature and
represent the main API to retrieve a project information. Other functions provide
additional features that are mostly useful during development and experimentation.
The reason for having a macro on top of the function is to enable some logic to
be executed at the call site, namely the retrieval of the caller classloader
which can be useful for some use cases (TODO: validate this in testing).
The macro is therefore a convenience that avoids the manual setting of the
:fmjrey.project/loader option.
Both macro and function take an optional argument which can be a single library
symbol in the format groupId/artifactId, or an options map which
may also specify a library symbol under the :lib key:
(def app-info (project/project-info 'lib/name))or
(def app-info (project/project-info {:lib 'lib/name}))Without any argument the owning project info is retrieved. With a library symbol given as a single argument or within the options map, it may be possible under certain conditions for a dependency to retrieve its own project info rather than the info from the dependent project. The locations where project data is searched for, and their applicability, are detailed in the Searched locations section.
When no symbol is given to project-info it can only search project data for the
running application in its runtime basis and project root directory and not in
a resource directory. It will also return the first project
data found regardless of its :id. For a more deterministic outcome it is best
to provide a symbol argument, and certainly necessary in the case of a library
code wishing to load its own project data instead of the dependent project data.
The search logic also tries to load a resource without specifying any
classloader, and then tries with an optional classloader if given with the
:fmjrey.project/loader option. The project-info macro adds the caller
classloader automatically, if not already provided.
A list of all possible options is detailed in the Options section.
In addition to the info macro and function the following functions take an options
map that may or may not have a :lib entry in the format groupId/artifactId.
They apply the same search logic as explained above for the macro, and return
the given option map possibly augmented with a :project/info entry if a
matching one is found.
read-project: returns the given option map augmented with a:project/infoentry if found, and stripped from all keys qualified withfmjrey.project.searched-deps: list alldeps.ednfiles or resource files checked for a matching project entry. This is mostly for experimentation and testing so as to check the different combinations of paths, files, and resources that are checked. Returns a sequence of options maps augmented with various working keys and a:project/infoentry when found.
For convenience at the command line, print-project and print-searched-deps
offer exactly the same functionality as their above counterparts, except they also
print to stderr the locations where a matching project entry is searched for.
To also print the matching project entries add :fmjrey.project/verbose :very
to the options map. Printing is in fact controlled by this option which is set
to true by the printing functions (unless :very is passed).
A list of all possible options is detailed in the Options section.
The most typical use of project info within build.clj is to create tasks
related to project versioning and release. For the latter the following function
is needed to create a copy of a project deps.edn to a library specific resource
directory:
copy-deps: copy the project rootdeps.ednto a resource directory. Options map may have a:libentry in the formatgroupId/artifactIdand an optional:fmjrey.project/resdirto specify the destination resource directory (defaults to"resources"). Return the options map unchanged. Internally this function uses thetools.deps.edn/project-deps-pathAPI (doc) to determine the path to the projectdeps.edn. Consequently the functionclojure.tools.deps.util.dir/with-dir, may be used to specify a custom project directory where to find the projectdeps.ednto copy, supporting custom setup such as polyfills or monorepos.
Functions mentioned in the previous section can also be run using the clojure CLI with the -T or -X option:
# copy project deps.edn to the resource directory
clojure -T:project copy-deps
> deps-edn-file ["deps.edn" file read-edn :aliases :project/info] available, found id my.app/name
Copying deps.edn to resources/deps/my/app/name/deps.edn
# read project deps.edn
clojure -X:project info
Searching for project info in alias :project/info
> deps-edn [current-basis :aliases :project/info] available, found id my.app/name
Found 1 matching source with :project/info
# read project deps.edn verbosely
clojure -X:project read-project :fmjrey.project/verbose :very
Searching for project info in alias :project/info
> deps-edn [current-basis :aliases :project/info] available, found id my.app/name:
{:id my.app/name,
:name "My project",
:license
{:id "EPL-2.0",
:name "Eclipse Public License 2.0",
:url "https://www.eclipse.org/legal/epl-2.0"}}
Found 1 matching source with :project/info in:
deps-edn [current-basis :aliases :project/info]
# list all searched locations
clojure -X:project searched-deps
Searching for project info in alias :project/info
> deps-edn [current-basis :aliases :project/info] available, found id my.app/name
> deps-edn [initial-basis :aliases :project/info] available, found id my.app/name
> deps-edn [current-basis :basis-config :project :aliases :project/info] not available (nil :aliases)
> deps-edn [project-deps :aliases :project/info] available, found id my.app/name
> deps-edn-file ["deps.edn" file read-edn :aliases :project/info] available, found id my.app/name
Found 4 matching sources with :project/info
By default project data is searched in the following locations in that order:
- Current basis
- Initial basis
- Current
:basis-config:projectmap deps.edncontent as provided bytools.deps.edn/project-depsAPI (doc). This meansclojure.tools.deps.util.dir/with-dirmay be used to specify a custom project directory.deps.ednas a file then as a resource/deps.ednas a resourcedeps/<group-id>/<artifact-id>/deps.ednas a resource/deps/<group-id>/<artifact-id>/deps.ednas a resource
To change the searched locations and their order set the option
:fmjrey.project/search-in to one of, or a vector of:
:basis: this corresponds to items 1 and 3 above:project: this corresponds to items 4 to 6 above:resource: this corresponds to items 7 and 8 above
For example to search only in the runtime basis:
(def app-info
(project/project-info {:lib 'my.app/name
:fmjrey.project/search-in :basis}))The table below summarizes the applicability of each above location along with
their corresponding ::search-in, ::source, and ::type option values.
The description of these last 2 options can be found in the
Customization section.
Disclaimer: there isn't at present a test case for each row, meaning this table
is made from the best knowledge of the author who would be happy to get
feedback and experience reports. Certainly rows having a note abbreviated by U
are not expected to be applicable in most cases, but may in some, and should
probably not be relied upon systematically unless it's the only option.
In any case passing a library symbol as a :lib parameter is the best way to
ensure more reliable results.
To save space the table uses abbreviations that are explained further below.
| # | Location | ::source | ::search-in |
as (::type) |
Applicability | From | For | If | Notes |
|---|---|---|---|---|---|---|---|---|---|
| 1 | Current basis | ['current-basis] |
:basis |
:deps-edn |
RUN, DEV | P, D | P | CLI, SRC | BA |
| 2 | Initial basis | ['initial-basis] |
:basis |
:deps-edn |
RUN, DEV | P, D | P | CLI, SRC | BA |
| 3 | Current :basis-config :project map |
['current-basis :basis-config :project] |
:basis |
:deps-edn |
RUN, DEV | P, D | P | CLI? | |
| 4 | clojure.tools.deps.edn/project-deps |
['project-deps] |
:project |
:deps-edn |
RUN, DEV | P, D | P | SRC | WD |
| 5 | deps.edn |
["deps.edn"] |
:project |
:deps-edn-file |
RUN, DEV | P, D | P | SRC | |
["deps.edn"] |
:project |
:deps-edn-rsrc |
RUN | P | P | JAR, UJAR | U, RO | ||
["deps.edn"] |
:project |
:deps-edn-rsrc |
RUN | D | I | JAR, DSRC | U, RO | ||
| 6 | /deps.edn |
["/deps.edn"] |
:project |
:deps-edn-rsrc |
RUN | P | P | JAR, UJAR | U, RO, RT |
["/deps.edn"] |
:project |
:deps-edn-rsrc |
RUN | D | I | JAR, DSRC | U, RO, RT | ||
| 7 | deps/<group-id>/<artifact-id>/deps.edn |
["deps/<group-id>/<artifact-id>/deps.edn"] |
:resource |
:deps-edn-rsrc |
RUN | P, D | P, D | JAR, UJAR | RO |
| 8 | /deps/<group-id>/<artifact-id>/deps.edn |
["/deps/<group-id>/<artifact-id>/deps.edn"] |
:resource |
:deps-edn-rsrc |
RUN | P, D | P, D | JAR, UJAR | RO, RT |
Applicability abbreviations
This table explains the applicability abbreviations with meaning to its right.
| Applicability | From | code in | For | getting project info of | If | launched/linked by | |
|---|---|---|---|---|---|---|---|
| N/A | Not Applicable | P | dependent project | P | dependent project | CLI | clojure CLI (project source dir) |
| RUN | Runtime | D | dependency | D | a dependency | SRC | in source dir |
| DEV | Dev+Ops | I | Itself | DSRC | deps from source (git or local, requires CLI) | ||
| JAR | jar file (and maven/clojars deps) | ||||||
| UJAR | uberjar file |
Notes abbreviations
| Abr. | Mnemonic | Notes |
|---|---|---|
| BA | Basis | The runtime basis is set as a JVM system property by the clojure CLI host-specific scripts, and is cached on disk (doc). |
| CLI? | CLI needed? | The clojure CLI does not seem to be needed for this, this case is more likely set programmatically (e.g. testing, custom config, etc.) |
| WD | With Dir | The function clojure.tools.deps.util.dir/with-dir (doc) may be used to specify a custom project directory. |
| U | Unlikely | Unlikely to be found unless explicitely added/copied in a resource root dir. Not really recommended but some existing project may have done so. |
| RO | Runtime Only | Intended for runtime use only. A resource deps.edn may be picked up at dev/ops time, but other locations should be preferred and searched first. |
| RT | RooT | A leading / designates the root of the classpath which may make sense in some environments and classloaders. |
NOTE An Excel spreadsheet was used to build the tables above is
in this repository under doc. To convert it to markdown tables use the
exceltk tool.
All API entry points can take an options map with the following optional entries:
-
:lib: a qualified symbol identifying the project, in the formatgroupId/artifactIdas expected under:libby write-pom. -
:fmjrey.project/search-in: specifies the locations, and in which order, where to search for project info, which can be one of, or a vector of::basis: current and initial basis:project: the project root:resource: the resource directory
See the Searched locations section for more details. Defaults to
[:basis :project :resource]. -
:fmjrey.project/alias: the alias name under which project info is captured, which is also used as the key for storing the matching project data in the returned options map instead of the default:project/info. -
:fmjrey.project/verbose: when true the progression of the search is printed, and when set to:veryit also prints the matching project entries. -
:fmjrey.project/loader: the classloader to also use for loading resources. Theproject-infomacro adds the caller classloader automatically, if one is not already provided. -
:fmjrey.project/resdir: only used bycopy-depsto specify the destination resource directory. Defaults to"resources".
More internally used options are explained in the Customization section.
The options described in the Options section should be the
first entry points for customization. In addition to these, more refined
customization can be achieved with the ::source option that is used
internally as a "mini-DSL" for describing a single project info location.
This capability was originally created to have a unique location identifier that also represent some of its constitutive parts, so as to enable finer generative testing logic. The code simplification it provided led to its evolution into a mini-DSL that drives the search for project info in all cases.
Exposing it as a customization feature may offer all the advantages DSLs can offer, notably a declarative approach, but also all the drawbacks in terms of abuse and potential frustrations with its narrow scope. It does offer some level of flexibility to deal with non-standard project configurations, as well as other files and formats for capturing project info that additional hosting platforms may already use. Nevertheless it's being presented here to elicit feedback on its relevance and possible usage.
The ::source option is used internally to represent unambiguously a
location where to search for project info. It must contain a vector of
literal values where symbols and keywords respectively evaluate to some
some retrieval logic and map values. The vector is interpreted from left
to right as if its elements were threaded with some->, meaning the result
of one token is passed to the next and any nil result stops the threading.
Here are how literals are handled:
-
Any literal that isn't of one of the type listed below just starts a new threading by ignoring the previous result and using itself as the result to be passed to the next step. For example many source vectors start with a string to be interpreted in the next step as a path to a
deps.ednfile. -
Keywords are applied as a function to the previous result in order to retrieve a map value.
-
Symbols are interpreted in a big
casestatement to apply specific logic. In case of an unknown symbol it starts a new threading, ignoring the previous result and passing itself to the next step. Below is a table of currently interpreted symbols:Symbol Triggered logic current-basisclojure.java.basis/current-basisinitial-basisclojure.java.basis/initial-basisproject-depsclojure.tools.deps.edn/project-depsfileclojure.java.io/fileresourceclojure.java.io/resourceresource-clclojure.java.io/resourcewith classloaderread-ednclojure.java.io/reader+clojure.tools.deps.edn/read-edn
The earlier location applicability table
gives for each location the initial ::source vector. It also gives the
internal ::type of the ::source, or more precisely the type of value that
should result from interpreting the corresponding ::source vector.
Below are the current types that can be used:
::type |
::source must evaluate to |
|---|---|
:deps-edn |
Any map that contains an :aliases entry |
:deps-edn-file |
A string representing the path to a deps.edn file |
:deps-edn-rsrc |
A string representing the path to a deps.edn resource file |
The ::type is also used to trigger additional logic before and after
interpreting the ::source vector. It is currently used to add additional
elements to the ::source vector, such as :aliases and :project/info
once a deps.edn map is loaded.
After interpretation it used to fine-tune or use the results, such as
adding new keys to the options map that will be returned.
For now there is no hook to handle additional ::type or tokens in the
::source mini-DSL. The interpretation logic is hard-coded in the
fmjrey.project/read-source function. Therefore the ::source option can
only be used to compose existing behavior, and an example of that can be
found in the fmjrey.project/copy function in order to get the
deps.edn path returned by
project-deps-path
(which works off the working directory set by
with-dir,
meaning custom deps setup such as polyfills or monorepos can be supported by
calling that with-dir function before this library).
Depending on interest and contributions the addition of such hooks may be considered, most likely using some additional options since multimethods, protocols, or records, may not be available in all clojure derivatives, whereas map literals are a defining feature of clojure that is unlikely to be missing.
Considering the recent increase in supply chain attacks, we should be extra
careful not to augment the risk surface. The limitations of using a very simple
::source mini-DSL may lead to the temptation of increasing its expressiveness.
This is exactly how the risk surface can be increased if not careful.
In particular, one obvious addition would be to interpret symbols as references to vars to be resolved at runtime. This increases the attack surface by providing a way to execute arbitrary code provided externally, ultimately pointing to the level of trust we can have on that code, and the mechanisms to bring it into the execution context.
We could restrict this feature to standard clojure tooling namespaces such as
clojure.tools.deps.edn and
clojure.tools.build.api.
The former is already required by this library in all cases, so it should not
expand the risk surface. The second however isn't, unless within build code.
So can something in non-build code fill the gap and parade as the build API?
Certainly one should prefer the use of
resolve over
requiring-resolve
so as to not go beyond what's already required at runtime.
Overall, if this mini-DSL remains an official way to customize the behavior of retrieving project info, we should strive to keep it simple and secure. As mentioned earlier, it is explained here to elicit feedback and debate.
Below are some work items remaining before some proper release:
More tests are needed, and in particular following the cases outlined in the
location applicability table.
An initial test framework with test projects inside the test-data folder
is in place, but such fixture requires launching the clojure CLI externally,
which is rather costly if done for each test case.
A more appropriate logic would be to have the code in these test projects run their own test cases and aggregate them back into this project testing. There is some scaffolding that can help:
- Each test project has the same code for invoking a function either in its own context, or by delegation in another test project added as a dependency. This is to enable testing when this project functions are called from both the owning project code and from a dependency code.
- The test code in this project has reproduced a simple version of the clojure
protocol
to programmatically invoke tools or functions externally via the clojure CLI.
On the caller side it unwraps the envelope created by the callee side.
In clojure these are respectively implemented in
tools.depsand theexec.jarfrom the clojure CLI which code seems to be here. However this clojure feature only works for invoking a project's own tools at runtime, and not another project or dependency tool or function.
Add documentation in the form of docstrings. This README is the only form of documentation at present, as it started to be written before any code, playing the role of a specification. Once the API is reasonably stable proper docstrings should be added.
This library overlaps with some intended work the clojure core team is pursuing or planning to pursue. In particular, there is an intent to make the clojure tooling more useful to, and support, different clojure dialects. This probably depends on the overall strategy for clojure evolution in relation to the various dialects it has generated. At the time of writing these workgroups are barely starting and won't provide definite feedback except some early hints. Nevertheless feedback from the core team will be essential for the evolution and stabilization of this library, which still needs to provide a sensible way to handle project info until such time there is an official one.
Until then here is the following feedback to the core team:
- Provide jar dependencies access to their own
deps.edn: this would probably be a new feature in the jar building logic. JIRA TDEPS-277 and TDEPS-278 hint at copyingdeps.ednas a resource file, and this project follows that idea. - Provide source dependencies access to their own
deps.edn: this would be a new feature to be provided by the clojure deps runtime. The alternative is using thedeps.ednresource copy, but this would force having that copy in source control, which opens a can of worms in terms of sync issues. - Provide a way to invoke externally any function from any project or jar:
the testing of this project required an enhanced copy of the clojure
protocol
to programmatically invoke functions in an external process via the clojure
CLI, see fmjrey/invoke for more details.
This is to ensure the function is executed with a runtime basis, working
directory, and
deps.edn, that are different from the calling project, whileinvoke-toolfrom clojure uses the runtime basis anddeps.ednof the calling project.
The last two items may hint at a more general feature for the clojure runtime to direct some of its logic to any project directory, or one of its dependency, and not just its own.
For this library evolution towards supporting more clojure dialects, the API and
terminology needs to be more generic and not specific to clojure on the JVM.
While the clojure core team will most likely be expanding the use of deps.edn,
it is not guaranteed that a project info is best placed in that file. For
integration purposes it may make more sense to leave it in a host-specific place.
Also dependencies may be coming from the host ecosystem itself, meaning not
written in a clojure dialect, and therefore without any deps.edn file.
Therefore the current use of the word deps in this project API may be too
specific and some review is likely to be needed at some point.
A library jar on clojars and some source dependency version mentioned in this README.
Convert some code into CLJC and add support for other clojure derivatives.
Invoke a library API function from the command-line:
$ clojure -X fmjrey.project/info :fmjrey.project/verbose :very
> deps-edn [current-basis :aliases :project/info] available, found id fmjrey/project:
{:id fmjrey/project,
:name "project",
:license
{:id "EPL-2.0",
:name "Eclipse Public License 2.0",
:url "https://www.eclipse.org/legal/epl-2.0"}}
Found 1 matching source with :project/info in:
deps-edn [current-basis :aliases :project/info]
Run the project's tests:
$ clojure -M:test
THE REST OF THIS SECTION WAS GENERATED BY THE TEMPLATE
TODO Update it!
Run the project's CI pipeline and build a JAR (this will fail until you edit the tests to pass):
$ clojure -T:build ci
This will produce an updated pom.xml file with synchronized dependencies inside the META-INF
directory inside target/classes and the JAR in target. You can update the version (and SCM tag)
information in generated pom.xml by updating build.clj.
Install it locally (requires the ci task be run first):
$ clojure -T:build install
Deploy it to Clojars -- needs CLOJARS_USERNAME and CLOJARS_PASSWORD environment
variables (requires the ci task be run first):
$ clojure -T:build deploy
Your library will be deployed to net.clojars.fmjrey/project on clojars.org by default.
This library is based on the following software and contributions
- Rich Hickey and the clojure core team: in an attempt to make this library
look like an extension of
tools.deps.edn, some minor amount of code has been copied from that library. - Macroz/tangle: the single namespace of that project has been duplicated and modified for generating graphs of the programmatically created test projects.
Copyright © 2026 François Rey
Distributed under the Eclipse Public License 2.0