Module overrides
Resolving module conflicts
Sometimes the information in a module descriptor is not correct, or must be customized.
- This is especially common when dealing with modules obtained from a Maven repository.
- It's also sometimes necessary when assembling a Ceylon application from third-party modules with conflicting dependencies.
Issues affecting Maven module dependencies
Maven module descriptors are notoriously unchecked, and many Maven module descriptors
are missing information about direct dependencies. These modules just happen to compile
and run by accident when using Maven, with its flat classpath containing all transitive
module dependencies. By contrast, Ceylon has ClassLoader
isolation, and therefore
requires correct and complete dependency information.
Furthermore, Maven does not support the notion of sharing module imports, so if a
module A
makes types from its imported module B
visible to the users of A
, the
import of B
must be made shared
. Sadly, Maven modules frequently bundle things
that should not be made visible, such as other modules or tests, that you surely
want to exclude, so making transitive dependencies shared
by default would be
inappropriate.
Finally, Maven supports module version conflict resolution by design, luck, or overrides, while Ceylon uses strict module version imports.
The overrides file
For all these reasons, we created an experimental measure that lets you override the
dependency information in a Maven or Ceylon module descriptor. The overrides.xml
file
allows us to:
- define constants and use them in interpolated XML attributes
- set a module version (for all modules)
- replace a module by another module (for all modules)
- remove a module (for all modules)
- add/remove module dependencies (per module)
- edit a module dependency, for example making it
shared
(per module) - include/exclude parts of the jar (for example, to exclude certain packages from a jar)
The format of the overrides.xml
file is defined by this XML schema.
Alternatives to the overrides file
The --use-flat-classpath
and --auto-export-maven-dependencies
options to the ceylon
command sometimes allow us to avoid the need to specify an overrides file, or allow us
to significantly simplify it.
You can also find these options in Ceylon IDE:
- on the Ceylon Build > Module Repositories page of the Project > Properties for your Ceylon project in Ceylon IDE for Eclipse, and
- on the Repositores tab of the Ceylon settings in the Project Structure in Ceylon IDE for IntelliJ.
Gotcha!
Note that --auto-export-maven-dependencies
does not automatically make all transitive
dependencies of any Maven module visible to Ceylon modules that import the Maven module!
It only makes all transitive dependencies visible to Maven modules themselves.
Overrides file syntax
The overrides file must be an XML file named overrides.xml
or maven-overrides.xml
(the name is not significant), valid according to the overrides schema.
For example:
<overrides xmlns="http://www.ceylon-lang.org/xsd/overrides">
<!-- Define a constant to be used in expressions -->
<define name="restletVersion" value="2.0.10"/>
<!-- Replace all versions of weld with version 1.1.4.Final -->
<set groupId="org.jboss.weld" artifactId="weld-osgi-bundle" version="1.1.4.Final"/>
<!-- Remove all uses of a module -->
<remove groupId="org.jboss.weld" artifactId="weld-osgi-bundle"/>
<!-- Add a module as a module root, to force it being loaded despite removal overrides in Maven -->
<add groupId="com.fasterxml.jackson.dataformat" artifactId="jackson-dataformat-xml" version="2.6.5"/>
<!-- Edit dependencies of org.restlet.jse:org.restlet/2.0.10 -->
<artifact groupId="org.restlet.jse" artifactId="org.restlet" version="${restletVersion}">
<!-- Add/replace a dependency -->
<add groupId="org.slf4j" artifactId="slf4j-api" version="1.6.1" shared="true"/>
<!-- Remove a dependency -->
<remove groupId="org.osgi" artifactId="org.osgi.core" version="4.0.0"/>
<!-- Share a dependency -->
<share groupId="org.slf4j" artifactId="slf4j-impl"/>
<!-- Override the default classifier, if required -->
<classifier>jar</classifier>
</artifact>
<!-- Replace all uses of org.apache.camel:camel-core/2.9.2 with version 2.10 -->
<replace groupId="org.apache.camel" artifactId="camel-core" version="2.9.2">
<with groupId="org.apache.camel" artifactId="camel-core" version="2.10"/>
</replace>
<!-- Edit dependencies of org.osgi:org.osgi.core/4.0.0 -->
<artifact groupId="org.osgi" artifactId="org.osgi.core" version="4.0.0">
<!-- Only include org/osgi/** and META-INF/** -->
<filter>
<!-- You can include or exclude and the matching is in sequence and stops at the first match -->
<include path="org/osgi/**"/>
<include path="META-INF/**"/>
<exclude path="**"/>
</filter>
</artifact>
</overrides>
Most ceylon
commands accept the --overrides
(or -O
) argument to specify this file.
ceylon compile --overrides=overrides.xml
Artifact coordinates or module names
Every element that works on modules accepts the expects XML attributes that identify the module or Maven artifact.
For a Ceylon module override:
-
module
specifies the Ceylon module name -
version
optionally specifies the module version
For a Maven module override:
-
groupId
specifies the Maven group id -
artifactId
specifies the Maven artifact id -
classifier
optionally specifies a Maven classifier
If version
is missing, the override will match all versions of the module or Maven
artifact.
Defining constants
Constants may be defined using the define
element:
<define name="version" value="2.0.10"/>
A constant may be used in any subsequent XML attribute with the ${constantName}
syntax:
<remove module="com.foo.bar" version="${version}"/>
Removing a module entirely
You can remove a module entirely from every import:
<remove module="com.foo.bar"/>
This element accepts the common module coordinate attributes.
Adding a module root
You can force a module being loaded as a module root, and prevent it from being removed by Maven overrides. This is useful to add modules to the classpath at run-time.
<add groupId="com.fasterxml.jackson.dataformat" artifactId="jackson-dataformat-xml" version="2.6.5"/>
This element accepts the common module coordinate attributes.
Overriding a module version globally
You can replace every import of a given module to use a specific version:
<set module="com.foo.bar" version="2"/>
This element accepts the common module coordinate attributes.
Replacing a module globally
You can replace every import of a given module to use another module:
<replace module="com.foo.bar" version="2">
<with module="com.foo.gee" version="3"/>
</replace>
These elements accept the common module coordinate attributes.
Overriding a single module's dependencies
<module module="com.foo.bar" version="2">
<!-- this will add or replace existing dependencies -->
<add module="com.foo.gee" version="3"/>
<remove module="com.foo.baz"/>
<share module="com.foo.dep"/>
</module>
Or for Maven artifacts:
<artifact groupId="com.foo" artifactId="bar" version="2">
<!-- this will add or replace existing dependencies -->
<add groupId="com.foo" artifactId="gee" version="3"/>
<remove groupId="com.foo" artifactId="baz"/>
<share groupId="com.foo" artifactId="dep"/>
</artifact>
These elements accept the common module coordinate attributes.
Overriding a single module's classifier
Some Maven modules require a custom classifier to be resolved properly:
<artifact groupId="org.wildfly.swarm" artifactId="swarmtool">
<!-- This will download the "-standalone" jar instead of the normal jar -->
<classifier>standalone</classifier>
</artifact>
Overriding a single module's version
As an alternative to using <set/>
to override a module version globally,
you can override the version of a specific version of a module:
<module module="com.foo.bar" version="2">
<!-- Use version 3 instead of 2 -->
<version>3</version>
</module>
Or of a Maven artifact:
<artifact groupId="com.foo" artifactId="bar" version="2">
<!-- Use version 3 instead of 2 -->
<version>3</version>
</artifact>
Filtering a single module's contents
Sometimes modules include classes you do not want to be seen at runtime, in the metamodel for example, as they are debug/test classes with missing dependencies. You can also exclude them with this:
<artifact groupId="org.osgi" artifactId="org.osgi.core" version="4.0.0">
<!-- Only include org/osgi/** and META-INF/** -->
<filter>
<!-- You can include or exclude and the matching is in sequence and stops at the first match -->
<include path="org/osgi/**"/>
<include path="META-INF/**"/>
<exclude path="**"/>
</filter>
</artifact>
Example (Google Guice)
Here's an overrides.xml
file that lets you import
Guice from Maven:
<overrides xmlns="http://www.ceylon-lang.org/xsd/overrides">
<module groupId="com.google.inject"
artifactId="guice"
version="4.0">
<share groupId="javax.inject"
artifactId="javax.inject"/>
</module>
</overrides>
You can now import
Guice like this:
native("jvm")
module com.my.app "1.0.0" {
import maven:com.google.inject:"guice" "4.0";
}
Note that you don't need this overrides.xml
file at all if you
use the --auto-export-maven-dependencies
flag which is supported
by the command line tools.
Example (Hibernate JPA solution 1)
Here's an overrides.xml
file that lets you import
Hibernate's JPA-compliant API from Maven:
<overrides xmlns="http://www.ceylon-lang.org/xsd/overrides">
<module groupId="org.hibernate"
artifactId="hibernate-entitymanager">
<share groupId="org.hibernate"
artifactId="hibernate-core"/>
<share groupId="org.javassist"
artifactId="javassist"/>
<share groupId="org.hibernate.javax.persistence"
artifactId="hibernate-jpa-2.1-api"/>
</module>
<module groupId="org.hibernate"
artifactId="hibernate-core">
<add groupId="javax.transaction"
artifactId="jta"
version="1.1"
shared="true"/>
</module>
</overrides>
Now you can import
Hibernate JPA like this:
native("jvm")
module com.my.app "1.0.0" {
import maven:org.hibernate:"hibernate-entitymanager" "5.0.4.Final";
import maven:org.hsqldb:"hsqldb" "2.3.1";
}
And define a persistence unit by placing this XML configuration in
resources/com/my/module/ROOT/META-INF/persistence.xml
where resources
is your Ceylon resources directory:
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="sample">
<class>com.my.app.Person</class>
<properties>
<property name="javax.persistence.jdbc.driver"
value="org.hsqldb.jdbcDriver"/>
<property name="javax.persistence.jdbc.url"
value="jdbc:hsqldb:mem:testdb"/>
<property name="javax.persistence.jdbc.user"
value="sa"/>
<property name="javax.persistence.jdbc.password"
value=""/>
<property name="hibernate.dialect"
value="org.hibernate.dialect.HSQLDialect"/>
<property name="hibernate.hbm2ddl.auto"
value="update"/>
</properties>
</persistence-unit>
</persistence>
Example (Hibernate JPA solution 2)
With this flag enabled, we can use the following simplified
overrides.xml
file:
<overrides xmlns="http://www.ceylon-lang.org/xsd/overrides">
<module groupId="org.hibernate"
artifactId="hibernate-core">
<add groupId="javax.transaction"
artifactId="jta"
version="1.1"
shared="true"/>
</module>
</overrides>
However, with this solution, we must explicitly import the JPA API
module, since the --auto-export-maven-dependencies
flag only affects
transitive dependencies.
native("jvm")
module com.my.app "1.0.0" {
import maven:org.hibernate:"hibernate-entitymanager" "5.0.4.Final";
import maven:org.hibernate.javax.persistence:"hibernate-jpa-2.1-api" "1.0.0.Final";
import maven:org.hsqldb:"hsqldb" "2.3.1";
}
Example (Spark Framework)
This overrides.xml
file allows Spark to be used from Maven:
<overrides xmlns="http://www.ceylon-lang.org/xsd/overrides">
<module groupId="org.eclipse.jetty"
artifactId="jetty-server"
version="9.3.2.v20150730">
<share groupId="org.eclipse.jetty"
artifactId="jetty-io"
version="9.3.2.v20150730"/>
</module>
<module groupId="org.eclipse.jetty"
artifactId="jetty-io"
version="9.3.2.v20150730">
<share groupId="org.eclipse.jetty"
artifactId="jetty-util"
version="9.3.2.v20150730"/>
</module>
<module groupId="org.eclipse.jetty"
artifactId="jetty-util"
version="9.3.2.v20150730">
<share groupId="javax.servlet"
artifactId="javax.servlet-api"
version="3.1.0"/>
</module>
</overrides>
Now we can import Spark like this:
native("jvm")
module sparky "1.0.0" {
import maven:com.sparkjava:"spark-core" "2.3";
}
Alternatively, if we use the --auto-export-maven-dependencies
flag, we don't need an overrides.xml
file at all. That's
probably a more robust solution in this case, given that Jetty
comprises a number of internal modules with inter-dependencies
that are not all captured in the Maven metadata.