
=head1 NAME

dh-clojure-lein - Debhelper support for Leiningen projects

=head1 DESCRIPTION

Support for Leiningen projects consists of a Debhelper build system,
available via the B<dh> B<--buildsystem> argument, and support for
automatic and customizable adjustments to projects.

Currently under development, and should not be considered stable until
(at least) the dh-clojure major version is no longer 0 and this notice
changes.

=head1 USAGE

=head2 Buildsystem

The build system is designed to facilitate packaging Leiningen-based
Clojure projects for Debian, and can be activated by passing the
B<--buildsystem=leiningen> option to B<dh>:

 %:
    dh $@ --buildsystem=leiningen

It takes action during these debhelper stages:

=over

=item B<configure>

Sets up a debian/dh-clojure/tmp/maven-repo symlink pointing to
/usr/share/maven-repo for use by lein as a :local-repo.

=item B<build>

Creates a debian/dh-clojure/tmp/pom.xml for use in the debian/PACKAGENAME.poms
file (which it also creates), and it creates an unversioned symlink in target/
for the jar.

=item B<install>

Calls helpers from the B<maven-repo-helper> package to install the
built jars into /usr/share/java and poms and jar symlinks in
/usr/share/maven-repo and generate the ${maven:Depends} substitution
variable.

=item B<test>

Runs C<lein test>.

=item B<clean>

Runs C<lein clean> and cleans up after the previous steps.

=back

=head2 Project Adjustment (middleware)

The Leiningen F<project.clj> will be automatically adjusted by the
buildsystem (see above).  By default, it will set all of the
dependency versions in the project to "debian", excepting
C<org.clojure/clojure>, which will be set to "1.x".

The project adjustments may be customized by code in an optional
F<debian/dh-clojure-lein.clj> file.  That file may define an
C<dh-clj-adjust-project> function which will be invoked like this:

  (dh-clj-adjust-project get-project when ...)

The first argument is a function that returns the Leiningen project
map.  The second argument indicates the current point in the project
build sequence.  At the moment it will be C<:before-plugins> or
C<:after-middleware>, but to allow for future changes, unexpected
values should be ignored, as should additional arguments.
C<dh-clj-adjust-project> must return the C<(get-project)> map after
any adjusments.  See EXAMPLES below.

An C<:after-middleware> call can assume that the project map has been
adjusted by a preceding C<:before-plugins> call, and a call for a
given keyword may be made more than once, so any actions must be
repeatable.

When making dependency adjustments, any changes to C<:plugins> should
be done during C<:before-plugins> invocations, and other dependency
changes should normally be made during C<:after-middleware>
invocations.

Note that all definition names starting with C<dh-clj> are reserved by
dh-clojure-lein.

In the project map passed to C<dh-clj-adjust-project> the values of the
dependency-related entries C<:dependencies>, C<:managed-dependencies>,
and C<:plugins> are not dependency vectors (as in F<project.clj>), but
are in leiningen's dependency *map* format, which is more suitable for
manipulation.  For example

  [[org.clojure/clojure "42.x"]]

is represented as

  {{:group-id "org.clojure", :artifact-id "clojure"}
   {:artifact-id "clojure", :group-id "org.clojure", :version "42.x"}}

Though in most cases, this won't be important because the following
client functions (defined in C<debian.dh-clojure-lein.client>) handle
the common project manipulations (see the EXAMPLES section below).
Note that only the public functions in C<client> are part of the
stable API.  Other functions in dh-clojure-lein should not be invoked
via C<dh-clojure-lein.clj>.

=over

=item (add-dep DEP-MAP DEP-SPEC)

Adds the dependency specified by DEP-SPEC (the dependency must not be
present in the original project).

=item (del-dep DEP-MAP DEP-KEY)

Removes the dependency specified by DEP-KEY (the dependency must be
present in the original project).

=item (set-dep DEP-MAP DEP-KEY FIELD VALUE)

Sets the specified FIELD (C<:version> or C<:exclusions>) of the
dependency specified by DEP-VEC to the given value (the dependency
must be present in the original project).  Setting C<:exclusions> to
C<nil> will remove all exclusions from the dependency.

=item (get-dep DEP-MAP DEP-SPEC [default])

Behaves exactly like C<clojure.core/get>, returning the C<{:group-id
...}> map in DEP-MAP, if any.

=back

A I<DEP-KEY> is the subset of a lein project dependency vector that
uniquely identifies a dependency, either a symbol like
C<org.clojure/tools.cli> or a a lein project dependency vector
starting with that symbol, optionally followed by a C<:classifier>,
and/or C<:extension>, for example C<[org.clojure/tools.cli :classifier
"test"]>.

A I<DEP-SPEC>s is a lein project dependency vector with the exception
that it may omit the version, for example C<[org.clojure/tools.cli
"1.x"]> or C<[org.clojure/tools.cli :classifier "test"]>.

Note that unless otherwise specified, the default value for all
dependencies will be set to either C<"debian">, or for
C<org.clojure/clojure>, to "1.x".

Some adjustments should only apply when specific profiles are active,
for example, removing a dependency in a test profile.  The active
profile(s) can be investigated via

=over

=item (active-profiles PROJECT)

Returns a sequence of the active profiles like C<(:base :system :user
:provided :dev)>.  So to check whether the C<:test> profile is active:
C<(some #{:test} (active-profiles PROJECT))>.

=back

Emacs clojure-mode and paredit (via elpa-clojure-mode and elpa-paredit
in Debian) can be quite helpful if you're editing a lot of these
files.  They provide automatic indentation, highlighting, structural
editing, etc.

=head1 EXAMPLES

=head2 Project adjustments

See L<the Clojure API|https://clojure.github.io/clojure/clojure.core-api.html#clojure.core>
for documentation of specific calls like C<assoc>, C<update>, and C<-E<gt>>.

Remove a plugin:

 (require '[debian.dh-clojure-lein.client :as deb])

 (defn dh-clj-adjust-project [get-project when & _]
   (if-not (= when :before-plugins)
     (get-project)
     (update (get-project) :plugins deb/del-dep 'lein-pprint)))

Adjust dependency:

 (require '[debian.dh-clojure-lein.client :as deb])

 (defn dh-clj-adjust-project [get-project when & _]
   (if (= when :after-middleware)
     (update (get-project) :dependencies deb/set-dep 'y :version "5.x"))
     (get-project))

Adjust more dependencies:

 (require '[debian.dh-clojure-lein.client :as deb])

 (defn adjust-deps [deps]
   (-> deps
       (deb/add-dep '[w "1.x"])
       (deb/add-dep 'org.clojure/core.match)
       (deb/del-dep 'murphy)
       (deb/set-dep 'y :version "5.x")
       (deb/set-dep 'y :exclusions nil) ;; delete exclusions
       (deb/set-dep '[x/y :classifier "test"] :version "5.x")))

 (defn dh-clj-adjust-project [get-project when & _]
   (if (= when :after-middleware)
     (update (get-project) :dependencies adjust-deps)
     (get-project)))

Equivalently:

 (require '[debian.dh-clojure-lein.client :as deb])

 (defn dh-clj-adjust-project [get-project when & _]
   (if-not (= when :after-middleware)
     (get-project)
     (-> (get-project)
         (assoc :version "11.x")
         (update :dependencies deb/add-dep '[w "1.x"])
         (update :dependencies deb/add-dep 'org.clojure/core.match)
         (update :dependencies deb/del-dep 'murphy)
         (update :dependencies deb/set-dep 'y :version "5.x")
         (update :dependencies deb/set-dep '[x/y :classifier "test"] :version "5.x"))))

Make profile-specific adjustments:

 (require '[debian.dh-clojure-lein.client :as deb])

 (defn adjust-deps [deps proj]
   (let [test? (some #{:test} (deb/active-profiles proj))
         dev? (some #{:dev} (deb/active-profiles proj))]
     (cond-> deps
       true (deb/add-dep '[w "1.x"])
       dev? (deb/add-dep 'a/b)
       test? (deb/del-dep 'murphy)
       true (deb/set-dep 'x :version "5.x")
       true (deb/set-dep '[x/y :classifier "test"] :version "5.x"))))

 (defn dh-clj-adjust-project [get-project when & _]
   (let [proj (get-project)]
     (if (= when :after-middleware)
       (update proj :dependencies adjust-deps proj)
       proj)))

Equivalently:

 (require '[debian.dh-clojure-lein.client
            :refer [active-profiles add-dep set-dep]])

 (defn fix-shared [deps]
   (-> %
       (add-dep '[w "1.x"])
       (set-dep 'x :version "5.x")
       (set-dep '[x/y :classifier "test"] :version "5.x")))

 (defn adjust-deps [deps proj]
   (let [test? (some #{:test} (active-profiles proj))
         dev? (some #{:dev} (active-profiles proj)])
     (cond
       test? (-> deps fix-shared (add-dep 'a/b))
       dev? (-> deps fix-shared (del-dep 'murphy))
       :else (fix-shared deps))))

 (defn dh-clj-adjust-project [get-project when & _]
   (let [proj (get-project)]
     (if (= when :after-middleware)
       (update proj :dependencies adjust-deps proj)
       proj)))

Remove a dependency that might not exist:

  (cond-> deps
    ...
    (deb/get-dep deps 'x/y) (deb/del-dep 'x/y)
    ...)

For debugging, you can use Clojure's existing print statements, for
example:

  (prn :active (active-profiles))

And for more readable output that can be inserted directly into the
threading forms (e.g. C<-E<gt>> and C<-E<gt><gt>>), you can do
something like this:

  (require 'clojure.pprint)

  (defn spy> [x tag]
    (binding [*out* *err*] (clojure.pprint/pprint [tag x]))
    x)

  (defn spy>> [tag x]
    (binding [*out* *err*] (clojure.pprint/pprint [tag x]))
    x)

 ...

 (defn fix-shared [deps]
   (-> %
       (add-dep '[w "1.x"])
       (spy> :after-w)
       (set-dep 'x :version "5.x")
       (spy> :after-version)
       (set-dep '[x/y :classifier "test"] :version "5.x")))

=head1 ENVIRONMENT

=over

=item B<DH_CLOJURE_INSTALL_TEST_JAR>

If the project is built to generate a "test" jar artifact, setting
this environment variable to C<true> will instruct dh-clojure to
include it in the package

=item B<DH_CLOJURE_LEIN_CREATE_MAVEN_REPO> (internal)

During testing this is set in the environment, and when set, causes
the buildsystem to use the value as the command to create the
C<debian/dh-clojure/tmp/maven-repo>.  This is used to provide a
writable C<maven-repo> instead of the normal symlink to
C</usr/share/...>.

=item B<DH_CLOJURE_LEIN_BUILD> (internal)

During the build, this is set to C<true> in C<lein>'s environment, and
when that's the case and dh-clojure is installed, Debian's C<lein>
will invoke C<debian.dh-clojure-lein/adjust-project> at various points
during the build to allow the calls to C<dh-clj-adjust-project>
described above.

Since Debian might not package some of the plugins, or might package
versions that differ from those specified in F<project.clj>
adjustments must be made before Leiningen attempts to find them.  That
means that they cannot be handled by a dh-clojure-lein middleware
(e.g. a plugin).

=item B<DH_CLOJURE_USJ_NAME>

By default, the buildsystem will use the project name as the
"usj-name" when writing out the package C<.poms> file.  This is the
filename for the jar that gets installed under C</usr/share/java>.

In some cases, two Java/Clojure projects share the same name, so this
can lead to package conflicts. In order to work around this, the
C<DH_CLOJURE_USJ_NAME> environment variable can be used to override
the "usj-name".

=item B<LEIN_HOME> (internal)

C<LEIN_HOME> is set to C<$(pwd)/debian/dh-clojure/tmp/lein-home>
in the environment during the build.

=item B<LEIN_OFFLINE> (internal)

During the build, C<LEIN_OFFLINE> is set to C<true> in the environment
to ensure that C<lein> doesn't attempt to retrieve any dependencies
from the network

=back

=head1 FILES

F<debian/dh-clojure-lein.clj>

=head1 SEE ALSO

L<dh-clojure(7)>, L<dh(1)>, L<lein(1)>, and
L<https://clojure.github.io/clojure/clojure.core-api.html>
