Grape Dependency Management
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0.0 (released 2026-09-22), run on JDK 26 and checked on 2026-10-14. Grape
(the Groovy Adaptable Packaging Engine) is the dependency manager that lets a single Groovy file declare the libraries it
needs: at compile time the @Grab annotation resolves the named artifact and its transitive dependencies from a Maven
repository, downloads the jars into a local cache and adds them to the class loader before the rest of the script is
resolved. A script can then be shipped on its own, which is why Grape is the usual answer to "how do I use a library from
a script". It is meant for scripts, tools and experiments. A compiled application or a Gradle or Maven project declares its
dependencies in its build, described on
Build Tools: Gradle, Maven & Ant.
Every example was run with groovy 6.0.0 from a scratch directory, and every JVM was started with
-Dgrape.root=<scratch>/grape-root (through JAVA_OPTS) so that the downloaded jars went to a throwaway directory and
not to ~/.groovy/grapes. The artifacts came from Maven Central. Output lines that the logging system wrote to standard
error and that carry no information (the WARNING lines the JVM prints for a mutated final field by the grape command,
for instance) are left out; the ones that matter are discussed where they appear.
In Groovy 6.0 Grape is split in modules: the facade (groovy.grape.Grape and the annotations) is in the core jar, the
Apache Ivy engine is groovy-grape-ivy (it was part of the core before 6.0) and a second engine based on Maven Resolver is
groovy-grape-maven, new and incubating in 6.0. Both jars are in the lib directory of the standard distribution. The
engines are covered in The two engines.
Your first @Grab
The shorthand form takes group:module:version. Put the annotation on an import, which is the form the documentation
recommends, because an annotated import is evaluated before the imported class is looked up:
@Grab('org.apache.commons:commons-text:1.13.0')
import org.apache.commons.text.WordUtils
def zones = ['north wing', 'south wing', 'seedling room']
zones.each { println WordUtils.capitalizeFully(it) }
assert WordUtils.capitalizeFully('north wing') == 'North Wing'
North Wing
South Wing
Seedling Room
The first run downloads commons-text and its dependency commons-lang3; later runs find them in the cache and start
without touching the network. The long form names the parts as attributes, which is useful when a value comes with a
classifier or other options:
@Grab(group = 'org.apache.commons', module = 'commons-text', version = '1.13.0')
import org.apache.commons.text.similarity.LevenshteinDistance
def distance = LevenshteinDistance.defaultInstance.apply('tomato', 'tomatoes')
println "distance = $distance"
assert distance == 2
distance = 2
Where the annotation can go
@Grab can annotate an import, a class, a method, a constructor, a field, a parameter or a local variable. The effect
is the same wherever it is: the resolution happens when the script or class is compiled, and a matching call is added to
the static initializer of the containing class (or of the script class for a script element). Annotations on the same
element can be repeated; the @Grapes container, needed in the very first versions of Groovy, is optional today. A
method and a local variable:
class Reporter {
@Grab('org.apache.commons:commons-text:1.13.0')
static String title(String s) {
org.apache.commons.text.WordUtils.capitalizeFully(s)
}
}
@Grab('commons-codec:commons-codec:1.17.1')
var digest = org.apache.commons.codec.digest.DigestUtils.sha256Hex('zone-1')
println Reporter.title('north wing')
println digest.take(16)
North Wing
88a58816fa1c5b56
The annotations have source retention only, so nothing about them is visible to reflection at run time.
The annotation family
| Annotation | Purpose |
|---|---|
|
One dependency: |
|
A container for several |
|
Adds a repository: |
|
Removes one transitive dependency from the resolution: |
|
Controls how the grab itself behaves: |
The fields come from the annotation types of the 6.0.0 distribution. @GrabExclude with a dependency that the code
really uses is shown on
AST Transformations:
Logging, Safety, Concurrency & Directives, together with the other @Grab annotations among the compiler directives.
Versions, the cache and the resolver chain
The version is a literal such as 1.13.0 or a range in Ivy notation: [1.16,1.18) is "1.16 up to but excluding 1.18",
[2.2.1,) is "2.2.1 or any greater version". A range resolved against a cache that already holds a matching version
picks from what is available, so a run can differ between machines. Pin an exact version in anything that must repeat:
@Grab('commons-codec:commons-codec:[1.16,1.18)')
import org.apache.commons.codec.binary.Hex
println Hex.encodeHexString('A1'.bytes)
4131
With the default engine the artifact was found as version 1.17.1, as grape list showed afterwards. The Ivy engine looks
in three places, in this order, as its verbose log lists them: cachedGrapes (the Grape cache), localm2 (the local
Maven repository, ~/.m2/repository) and ibiblio (Maven Central). The local Maven repository is a convenient source,
but it also means a half-populated Maven cache can fool a resolution; see Hardening.
The cache lives under the Grape root, ~/.groovy by default, in a directory named after the engine: grapes for
the Ivy engine and grapesM2 for the Maven one. The grape.root system property moves the root, which is how the runs
on this page were isolated. With -Dgrape.root=<scratch>/grape-root the first script above left this layout (shortened):
grape-root/grapes/org.apache.commons/commons-text/ivy-1.13.0.xml
grape-root/grapes/org.apache.commons/commons-text/jars/commons-text-1.13.0.jar
grape-root/grapes/org.apache.commons/commons-lang3/jars/commons-lang3-3.17.0.jar
Sharing one cache between a team’s machines, putting it on a CI cache volume, or deleting it to force a fresh download are all done through this directory. Deleting one module directory is enough to make Grape fetch that module again.
Additional repositories with @GrabResolver
Maven Central is searched by default. @GrabResolver adds another repository, such as a company’s Nexus or Artifactory
server or a file system directory. This example uses a tiny repository created for the page: a one-class jar and its POM in
Maven layout under a local directory (the directory was in the scratch area; it appears below as /path/to/local-repo):
@GrabResolver(name = 'greenhouse-local', root = 'file:///path/to/local-repo')
@Grab('com.example:greenhouse-sensors:1.0.0')
import greenhouse.Sensors
println Sensors.describe('north wing', 0.27)
assert Sensors.describe('north wing', 0.27) == 'north wing: soil moisture 0.27'
north wing: soil moisture 0.27
The jar and the POM were found at com/example/greenhouse-sensors/1.0.0/ below the root, the path a Maven repository
uses. The Ivy engine, while setting a custom resolver up, wrote a problems summary with the line unknown resolver
greenhouse-local to standard error on the first run, and then resolved the artifact and ran the script anyway; the same
script with the Maven engine printed nothing of the kind. Treat it as noise when the script ends with the expected
result, and as a real problem when the resolution reports that the module could not be found. A resolver whose name
collides with a built-in one (central, for instance) is a real error, as the page on the AST transformations shows.
Pick a distinct name.
The root should be an https:// URL. A plain http:// root is reported by the hardening checks described below because
anything fetched over it can be read or changed on the way.
Classifiers, excludes and transitive dependencies
A Maven classifier picks a variant of the same coordinates, such as a tests or sources jar. Name it as the fourth
element of the shorthand or in the classifier attribute:
@Grab(group = 'org.apache.commons', module = 'commons-collections4', version = '4.4', classifier = 'tests')
import org.apache.commons.collections4.AbstractObjectTest
println AbstractObjectTest.protectionDomain.codeSource.location.path.split('/').last()
commons-collections4-4.4-tests.jar
A dependency brings its own dependencies along unless transitive = false says otherwise. To see exactly which jars an
artifact brings, ask the facade, which returns the jar locations:
import groovy.grape.Grape
def args = [group: 'org.apache.commons', module: 'commons-text', version: '1.13.0']
def all = Grape.instance.resolve([:], args)
def alone = Grape.instance.resolve([:], args + [transitive: false])
println all*.toString().collect { it.split('/').last() }
println alone*.toString().collect { it.split('/').last() }
[commons-text-1.13.0.jar, commons-lang3-3.17.0.jar]
[commons-text-1.13.0.jar]
@GrabExclude is the finer tool: it drops one transitive module and keeps the rest, which is the way to stay on a version
of a library that the application already provides, or to remove a module that would clash. The attributes are
group and module, or @GrabExclude('xml-apis:xml-apis') for the shorthand. The effect on the classpath is
the effect of removing the jar, so code that needs the class fails with NoClassDefFoundError, as the page linked in the
previous section demonstrates.
JDBC drivers and @GrabConfig
@GrabConfig configures the grab. Its most famous option is systemClassLoader = true, which attaches the jars to the
system class loader instead of the loader of the script. It exists because of how JDBC finds drivers: DriverManager
only offers a driver to code that can see the driver’s class loader. A driver grabbed into the script’s class loader is
invisible to groovy.sql.Sql, which is loaded by the Groovy runtime. With H2 (an embedded database, see
Databases with Groovy SQL):
@GrabConfig(systemClassLoader = true)
@Grab('com.h2database:h2:2.3.232')
import groovy.sql.Sql
def sql = Sql.newInstance('jdbc:h2:mem:greenhouse')
sql.execute('create table zone (name varchar(20), soil decimal(4,2))')
sql.execute("insert into zone values ('north wing', 0.27)")
sql.eachRow('select name, soil from zone') { println "${it.name} ${it.soil}" }
sql.close()
north wing 0.27
The same script without the @GrabConfig line fails, in this setup, with:
java.sql.SQLException: No suitable driver found for jdbc:h2:mem:greenhouse
at java.sql/java.sql.DriverManager.getConnection(DriverManager.java:638)
at groovy.sql.Sql.newInstance(Sql.java:353)
Calling DriverManager.getConnection directly from the script did work without it, because the script’s own loader
could see the driver. The other options of @GrabConfig:
| Option | Effect |
|---|---|
|
Adds the jars to the system class loader. Needed for JDBC drivers and for libraries that look themselves up through it. |
|
Sets system properties for the grab, as |
|
Also sets the thread context class loader to the loader that received the jars. |
|
|
|
Skips the verification of the downloaded checksums. Turn it on only to get past a broken mirror, never as a default. |
Grape from code
The annotation is the compile-time front end of a facade that can also be called directly. Grape.grab takes a map with
group, module, version and optionally classifier, conf (Ivy configuration or Maven scope), force,
changing and transitive. There are synonyms for the keys (groupId, organisation, organization, org for the
group; artifactId and artifact for the module; version, revision and rev), and giving two synonyms at once is a
runtime error. Because the call happens at run time, the classes are not known to the compiler: reach them with
Class.forName or loadClass, or split the script so that the code using them is evaluated later.
import groovy.grape.Grape
Grape.grab(group: 'org.apache.commons', module: 'commons-text', version: '1.13.0')
def wordUtils = Class.forName('org.apache.commons.text.WordUtils')
println wordUtils.capitalizeFully('seedling room')
// the map synonyms: groupId/artifactId/revision
Grape.grab(groupId: 'commons-codec', artifactId: 'commons-codec', revision: '1.17.1')
println Class.forName('org.apache.commons.codec.binary.Hex').encodeHexString('A1'.bytes)
// a missing artifact: silence the exception with noExceptions
def ok = Grape.grab(noExceptions: true, group: 'com.example', module: 'no-such-module', version: '9.9.9')
println "noExceptions result: $ok"
try {
Grape.grab(group: 'com.example', module: 'no-such-module', version: '9.9.9')
} catch (RuntimeException e) {
println "failed: ${e.message.readLines().first()}"
}
Seedling Room
4131
noExceptions result: null
failed: Error grabbing Grapes -- [unresolved dependency: com.example#no-such-module;9.9.9: not found]
The jars go to a class loader that must be a RootLoader or a GroovyClassLoader, found in this order: the one passed
as classLoader:, the loader of the object passed as refObject:, and the loader of the class that calls grab. A
script run by the groovy command has one. In an application that embeds Groovy, create a GroovyClassLoader, pass it,
and load the classes through it:
import groovy.grape.Grape
def loader = new GroovyClassLoader()
Grape.grab(classLoader: loader, group: 'org.apache.commons', module: 'commons-text', version: '1.13.0')
def cls = loader.loadClass('org.apache.commons.text.WordUtils')
println cls.capitalizeFully('seedling room')
try {
Class.forName('org.apache.commons.text.WordUtils')
} catch (ClassNotFoundException e) {
println "not visible to the default loader: ${e.message}"
}
Seedling Room
not visible to the default loader: org.apache.commons.text.WordUtils
The second map argument and its options (validate, noExceptions) belong to the two-map form
Grape.grab(args, dependency1, dependency2, ...); a call with a single map uses it as both. Embedding is the subject of
Integrating Groovy into Java
Applications.
Grape in groovysh
In the shell there are no annotations on imports to rely on, since every input is evaluated on its own. The documentation
suggests the method call; groovysh also has a /grab command that wraps it (see
groovysh & groovyconsole). Both were run by piping
the lines to groovysh -q -C false:
groovy> groovy.grape.Grape.grab(group:'org.apache.commons', module:'commons-text', version:'1.13.0')
groovy> org.apache.commons.text.WordUtils.capitalizeFully('north wing')
North Wing
groovy> /exit
groovy> /grab org.apache.commons:commons-text:1.13.0
groovy> org.apache.commons.text.WordUtils.capitalizeFully('south wing')
South Wing
groovy> /exit
The groovyConsole has no such command; start it with a script that holds the @Grab annotation, or call Grape.grab
in the first lines of the script window.
The grape command
The grape executable in $GROOVY_HOME/bin inspects and manages the cache. Its commands in 6.0.0:
| Command | Purpose |
|---|---|
|
Downloads the module and its dependencies into the cache. Without a version the latest one is used. |
|
Lists the modules in the cache with their versions. |
|
Prints the jar locations of the module and its transitive dependencies, ready for use in another tool: |
|
Removes the module’s jar from the cache, non-transitively. |
The global options are -q, -w, -i, -V and -d (log level 0 to 4), -D to define a system property, -r to add
a resolver for an install, -h and -v. Since 6.0 install also accepts the coordinate shorthands of @Grab. The three
forms below are equivalent; the third one is the Ivy notation group#module;version, and a classifier and an extension
are written group:module:version:classifier@ext (this form is from the 6.0 release notes):
$ grape -q install org.apache.commons commons-text 1.13.0
$ grape -q install org.apache.commons:commons-text:1.13.0
$ grape -q install 'org.apache.commons#commons-text;1.13.0'
A session with a fresh cache (the cache path is shortened to <grape.root>):
$ grape -q install commons-codec:commons-codec:1.17.1
$ grape -q install org.apache.commons commons-text 1.13.0
$ grape -q install org.apache.commons:commons-collections4:4.4:tests
$ grape list
commons-codec commons-codec [1.17.1]
org.apache.commons commons-text [1.13.0]
org.apache.commons commons-lang3 [3.17.0]
org.apache.commons commons-collections4 [4.4]
org.apache.commons commons-parent [48, 71]
org.apache apache [21, 32]
org.junit junit-bom [5.11.0-M2]
7 Grape modules cached
9 Grape module versions cached
$ grape resolve -s org.apache.commons commons-text 1.13.0
export CLASSPATH=<grape.root>/grapes/org.apache.commons/commons-text/jars/commons-text-1.13.0.jar:<grape.root>/grapes/org.apache.commons/commons-lang3/jars/commons-lang3-3.17.0.jar
$ grape -q uninstall commons-codec commons-codec 1.17.1
Deleting commons-codec-1.17.1.jar
grape install is the way to warm a cache in advance, for instance in the image build of a container that later runs
scripts without network access. grape resolve turns a Grape dependency into a plain classpath for Java, Ant or a shell
script. uninstall deletes the jar but the module still shows in list, with an empty version list; remove the module
directory by hand when you want it to disappear. Without -q the Ivy engine prints its resolution messages at INFO
level, which is noisy in scripts.
The two engines
Groovy 6.0 ships two implementations of the Grape engine behind the same annotations and facade (incubating in 6.0):
| Aspect | GrapeIvy (default) |
GrapeMaven (new) |
|---|---|---|
Backend |
Apache Ivy |
Maven Resolver |
Engine class |
|
|
Module |
|
|
Cache |
|
|
Configuration file |
|
none; use |
|
Ivy configurations; lists such as |
a single Maven scope; |
|
honoured, cache-only resolution |
not honoured |
Version |
Ivy |
Maven |
When both engines are on the classpath, GrapeIvy wins. Select the other one with the groovy.grape.impl system property,
on the command line or globally in JAVA_OPTS. A script that prints which engine served it, run twice against the same
Grape root:
@Grab('org.apache.commons:commons-text:1.13.0')
import groovy.grape.Grape
import org.apache.commons.text.WordUtils
println "engine: ${Grape.instance.class.name}"
println WordUtils.capitalizeFully('north wing')
$ groovy engine.groovy
engine: groovy.grape.ivy.GrapeIvy
North Wing
$ groovy -Dgroovy.grape.impl=groovy.grape.maven.GrapeMaven engine.groovy
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
engine: groovy.grape.maven.GrapeMaven
North Wing
Source: Groovy 6.0 release notes: Grape: Dual Engine Support, Groovy 6.0 release notes: Selecting the engine
Afterwards the Grape root held both caches side by side: grapes from the first run and grapesM2 (with
org/apache/commons/commons-text/1.13.0/commons-text-1.13.0.jar below it) from the second. The SLF4J lines come from
Maven Resolver, which looks for a logging binding; they are harmless. Both engines honour grape.root. The local file
repository script from the section on @GrabResolver also ran unchanged with the Maven engine.
A value of groovy.grape.impl that names a class that does not exist leaves no engine to resolve the annotation, and the
script fails at compile time with unable to resolve class for the imports it was meant to supply. A custom engine is a
class implementing groovy.grape.GrapeEngine, registered in META-INF/services/groovy.grape.GrapeEngine of a jar on
the classpath and selected with the same property; if it is the only one on the classpath it is found without it.
Moving a script between engines
Most scripts work unchanged. The differences to check, all from the table above, are conf: lists (rework them to a
single scope, or stay with Ivy), a ~/.groovy/grapeConfig.xml with custom Ivy settings (re-register the repositories
with @GrabResolver), and autoDownload = false. The last one is easy to observe. With the artifact missing from the
cache, the Ivy engine honours the flag and refuses to download, and the Maven engine ignores it and downloads:
@GrabConfig(autoDownload = false)
@Grab('commons-beanutils:commons-beanutils:1.9.4')
import org.apache.commons.beanutils.BeanUtils
println BeanUtils.class.name
$ groovy offline-miss.groovy (Ivy engine, empty cache)
...
General error during conversion: Error grabbing Grapes -- [unresolved dependency: commons-beanutils#commons-beanutils;1.9.4: not found]
$ groovy -Dgroovy.grape.impl=groovy.grape.maven.GrapeMaven offline-miss.groovy (Maven engine, empty cache)
org.apache.commons.beanutils.BeanUtils
With the artifact already in the cache the same flag lets the script run with no network, which was the case for a copy of the first script in this page:
@GrabConfig(autoDownload = false)
@Grab('org.apache.commons:commons-text:1.13.0')
import org.apache.commons.text.WordUtils
println WordUtils.capitalizeFully('cached copy')
Cached Copy
For offline use on the Maven engine, warm the cache with grape install while online and then point @GrabResolver
at a local-only repository, as the release notes recommend, or stay on the Ivy engine.
Proxies and logging
Behind a proxy, pass the standard JVM properties, on the command line or for every run in JAVA_OPTS:
groovy -Dhttp.proxyHost=proxy.internal -Dhttp.proxyPort=8080 report.groovy
JAVA_OPTS="-Dhttp.proxyHost=proxy.internal -Dhttp.proxyPort=8080"
This was not run here (it needs a proxy); the host names are placeholders. For https repositories the matching
https.proxyHost and https.proxyPort properties apply as for any JVM program. Credentials for a repository that needs
them do not belong in the script or in an annotation; they belong in the build tool or in the environment, and a script
that needs a private repository is better turned into a Gradle or Maven project.
To see what Grape is doing, the Ivy engine accepts -Dgroovy.grape.report.downloads=true, which prints one line for each
resolve and download to standard error. Against an empty cache:
Resolving dependency: org.apache.commons#commons-text;1.13.0 {default=[default]}
Preparing to download artifact org.apache.commons#commons-text;1.13.0!commons-text.jar
Preparing to download artifact org.apache.commons#commons-lang3;3.17.0!commons-lang3.jar
Downloaded 909 Kbytes in 7ms:
[SUCCESSFUL ] org.apache.commons#commons-text;1.13.0!commons-text.jar (1ms)
[SUCCESSFUL ] org.apache.commons#commons-lang3;3.17.0!commons-lang3.jar (1ms)
(The download here came from the local Maven repository, hence the short times.) Since 6.0 Groovy routes its diagnostics
through the JDK’s platform logging, so the detailed output is controlled with a logging.properties file: Groovy reads
~/.groovy/logging.properties or the file named by -Djava.util.logging.config.file. A file that raises the Ivy engine’s
logger to FINE:
handlers = java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = ALL
groovy.grape.ivy.level = FINE
printed, among many lines, the resolver chain and each step of the resolution:
FINE: default resolver: downloadGrapes
FINE: -- 4 resolvers:
FINE: downloadGrapes [chain] [cachedGrapes, localm2, ibiblio]
FINE: == resolving dependencies caller#all-caller;working98->org.apache.commons#commons-text;1.13.0 [default->default]
The levels are INFO, FINE and FINEST for increasing detail, and the grape command’s -q, -w, -i, -V and -d
flags adjust the Ivy level for one invocation. The Maven engine ignores groovy.grape.report.downloads; its switch is
-Dgroovy.grape.debug=true, which prints lines such as:
[GrapeDebug] resolving root=org.apache.commons:commons-text:jar:1.13.0 transitive=true
[GrapeDebug] resolved org.apache.commons:commons-lang3:jar:3.17.0 -> <grape.root>/grapesM2/org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar
Hardening
Grape downloads code and puts it on the class path of a program that is about to run it. Whatever the repository serves, it
executes with the rights of the script. Three habits matter more than any option: pin exact versions, use only
repositories you control or trust, and prefer https://. Groovy 6.0 added several checks of its own:
-
A coordinate part that contains a path separator or a
..segment is rejected before it can reach the cache layout:-
Grab: invalid value of '../../etc' for module: should only contain - . _ a-z A-Z 0-9
-
-
A resolver root that uses a plaintext protocol is reported. The policy is chosen with
-Dgroovy.grape.insecureProtocolPolicy:warn(the default) logs it and adds the resolver,failrefuses it,ignoreskips the check. Loopback hosts are exempt under every policy. -
The Maven engine now fails the resolution on a checksum mismatch, as the Ivy engine always did. Artifacts published with no checksum still resolve.
@GrabConfig(disableChecksums = true)or-Dgroovy.grape.disableChecksumsturn the verification off completely, which defeats the check. -
The strict resolvers for the local Maven repository and for the Grape cache reject half-populated entries (a POM with no jar, a corrupt jar), so a damaged entry is fetched again; a relaxed configuration restores the old behaviour.
-
A corrupt or truncated jar no longer aborts the whole
@Grab: extension-method scanning skips it with a warning. -
grape uninstalldeletes only inside the module cache, checked on canonical paths.
The first two bullets were run. The coordinate check and the two policies against Grape.addResolver:
import groovy.grape.Grape
try {
Grape.grab(group: 'org.apache.commons', module: '../../etc', version: '1.0')
} catch (RuntimeException e) {
println "rejected: ${e.message.readLines().first()}"
}
Grape.addResolver(name: 'plain', root: 'http://repo.example.test/maven2/')
Grape.addResolver(name: 'loopback', root: 'http://localhost:8081/maven2/')
println 'resolvers added'
$ groovy hardening.groovy
rejected: Grab: invalid value of '../../etc' for module: should only contain - . _ a-z A-Z 0-9
resolvers added
$ groovy -Dgroovy.grape.insecureProtocolPolicy=fail hardening.groovy
rejected: Grab: invalid value of '../../etc' for module: should only contain - . _ a-z A-Z 0-9
java.lang.RuntimeException: Grape resolver 'plain' uses the plaintext root 'http://repo.example.test/maven2/' and was rejected because -Dgroovy.grape.insecureProtocolPolicy=fail is set. ...
at groovy.grape.Grape.checkResolverRootProtocol(Grape.java:451)
With the default warn policy the plaintext resolver was added after a log line from checkResolverRootProtocol, and the
loopback one never triggered the check. The host is a reserved test name; the script never connected to it. Checksum and
cache handling are described in the release notes and were not exercised here.
Finally, -Dgroovy.grape.enable=false switches @Grab off. A script that depends on one fails to compile, with the
import it was meant to provide reported as unable to resolve class. That is the switch for an environment (a locked-down
CI job or a server that compiles user scripts) where code must not fetch anything. Compile-time restrictions for scripts
that come from outside are described on
Compilation Customizers & Secure
Scripting.
Where the book is outdated
The first edition of the reference book (Groovy 1.0, 2007) predates Grape altogether: Grape arrived in Groovy 1.6 (2009).
A script of the book’s era placed jars on the command line with -cp, in $GROOVY_HOME/lib or in ~/.groovy/lib, and
that still works but is the opposite of a self-contained script. What differs now: the @Grab annotation on an import, the
shorthand coordinates and the grape command, the grape.root property to move the cache, and, new in 6.0, the split of
the Ivy engine into its own module, the second engine based on Maven Resolver, platform logging, the shorthands of
grape install and the hardening checks. Documentation written for 2.x or 3.x still describes the Ivy engine only, the
groovy.grape.report.downloads property as the logging switch and ~/.groovy/grapeConfig.xml as the way to configure it;
those remain true for GrapeIvy but not for the new engine. Examples that grab a http:// repository are also dated,
since Maven Central itself is https only and the plaintext check now warns. The collected list of changes is on
What’s New & Migrating from Older
Versions.