Martin Carpenter
Martin Carpenter

Reputation: 5951

Dynamic leiningen :profiles

I'm trying to use a function as the value for the :profiles key in a defproject form. Starting from a fresh project (lein new app test) this works fine:

:profiles {}

(as you might hope!). But if I change it to:

:profiles (merge {})

then when I run lein repl it explodes:

Caused by: java.lang.ClassCastException: clojure.lang.Symbol cannot be cast to java.util.Map$Entry

I'm confused by this since if I set :profiles back to the empty map and ask the repl these things are equal:

test.core=> (= {} (merge {}))
true

Where is my misunderstanding? Have I missed something basic? Is this an unfortunate artifact of the defproject macro? Something else?

(clojure 1.8.0, leiningen 2.7.1, java 1.8.0_102)


Edit - working solution with Scott's answer:

(def project-name 'myproj)
(def mains ["foo" "bar"])
...
(defn- lein-alias [main]
  { main ["with-profile" main] })

(defn- lein-profile [main]
  (let [jar      (str main ".jar")
        entry    `~(str project-name "." main)]
    {(keyword main) {:main          entry 
                     :bin           {:name main}
                     :jar-name      jar
                     :uberjar-name  jar}}))

(defproject project-name "0.1.0"
...
  :profiles ~(apply merge (concat (map lein-profile mains) {:uberjar {:aot :all}}))
  :aliases ~(apply merge (map lein-alias mains))
  ...

So now I can lein foo bin and lein bar bin to my heart's content.

Upvotes: 2

Views: 1909

Answers (2)

Scott
Scott

Reputation: 1688

If you unquote your form Leiningen will execute the form before the project map is evaluated.
So ~(merge {}) should work.

There is a function called unquote-project in Leiningen src/leiningen/core/project.clj#L176

"Inside defproject forms, unquoting (~) allows for arbitrary evaluation."

It looks like it looks items to unquote to allow them to be execute. Based on the comment it looks like this might go away in 3.0 and suggests use read-eval syntax


Note: If you are just trying to merge values of different profiles you should look at the Profiles documentation About Merging and Composite Profiles

Composite Profiles

Sometimes it is useful to define a profile as a combination of other profiles. To do this, just use a vector instead of a map as the profile value. This can be used to avoid duplication:

{:shared {:port 9229, :protocol "https"}
 :qa [:shared {:servers ["qa.mycorp.com"]}]
 :stage [:shared {:servers ["stage.mycorp.com"]}]
 :production [:shared {:servers ["prod1.mycorp.com", "prod1.mycorp.com"]}]}

Upvotes: 3

Alan Thompson
Alan Thompson

Reputation: 29966

You do not merge the profiles manually. They are merged together by lein either automatically (normal case) or when you use the with-profile keyword (manual control).

For example, consider this project.clj:

(defproject xyz "0.1.0-SNAPSHOT"

  :dependencies [ [org.clojure/clojure "1.8.0"] 
                  [tupelo "0.9.19"] ]

  :profiles {:dev     {:dependencies [ [org.clojure/test.check "0.9.0"]
                                       [criterium "0.4.4"] ] }
             :sample  {:dependencies [medley "0.8.2"] }
  ...
)

This project.clj says that the project always requires both org.clojure/clojure and tupelo. During development, the map for :dev will be merged into the root-level, so :dependencies will be updated to include both test.check and criterium. The :dev profile values are not included when a uberjar is created, however, so these libs won't be included in the code delivered to users.

Since :sample is not one of the default profiles, it will only be included if you use a command like:

> lein with-profile sample test

Notice that the leading colon is not included on the command line, although we use a keyword :sample with the colon in the project.clj file.

Full details are here: https://github.com/technomancy/leiningen/blob/master/doc/PROFILES.md#default-profiles

and here: https://github.com/technomancy/leiningen/blob/master/sample.project.clj

Having said all this, I normally don't need to use :profiles. Unless you have something more complex than usual, you should usually just put all of your dependencies at the root level (i.e. the :dependencies keyword at the first level under (defproject xyz ...) in the project that lein new app xyz gives you.

Another suggestion: the word test is used in many places in a lein project (directory name, file name suffix, and others), so it can be very confusion to name the project itself test! You'll save yourself (and any other readers) some grief if you choose a unique name like xyz, joe, or anything else.

Upvotes: 2

Related Questions