Ant Properties

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 Ant 1.10.18. Every example was run on JDK 26 on 2026-10-16 with that release, in a scratch directory that mimics the tidepool model module. Paths in the console output were shortened to /work/properties/..., and the Ant installation to /opt/apache-ant-1.10.18. Ant’s own warnings and error messages are quoted exactly as printed.

Properties are the one feature every Ant build uses on its first line and the one whose rules surprise newcomers most. They look like variables, and they are not: they are named, string-valued constants that, once set, cannot be changed. Almost every "why does my build ignore this setting?" question about Ant comes down to the rules explained here. For the other pieces of a build file see Build File Structure and Targets & Extension Points; for the command-line switches that define properties see Running Ant.

What a property is

A property is a case-sensitive name with a string value. Wherever a task attribute or a task’s nested text contains ${name}, Ant replaces it with the value at the moment the task is configured. Ant also exposes every JVM system property (${os.name}, ${user.home}, ${java.version}) as if it had been set with the property task.

Three rules govern everything else:

  1. Properties are immutable. The first definition wins; every later attempt to set the same name is silently ignored. The manual says it directly: whoever sets a property first freezes it for the rest of the build, and properties are "most definitely not variables".

  2. Properties are global. Once defined they are visible to every task in every target that runs afterwards, and the child builds started by ant, antcall and subant cannot set a property that the calling build then sees. The one exception to global scope is the local task, described below.

  3. Properties are expanded when they are set or read, once. A value that contains a reference to a property that is not yet defined keeps the text ${...} literally.

First definition wins

<project name="tidepool-model" default="show" basedir=".">
  <property name="build.dir" value="build"/>
  <property name="build.dir" value="out"/>
  <property name="classes.dir" value="${build.dir}/classes"/>
  <property name="dollar.demo" value="$${build.dir} is ${build.dir}"/>

  <target name="show">
    <echo>build.dir   = ${build.dir}</echo>
    <echo>classes.dir = ${classes.dir}</echo>
    <echo>${dollar.demo}</echo>
    <echo>$${not.defined} stays: ${not.defined}</echo>
  </target>
</project>

Run with no arguments (ant):

Buildfile: /work/properties/ex1/build.xml

show:
     [echo] build.dir   = build
     [echo] classes.dir = build/classes
     [echo] ${build.dir} is build
     [echo] ${not.defined} stays: ${not.defined}

BUILD SUCCESSFUL
Total time: 0 seconds

The second build.dir definition (out) was ignored. Nothing was printed about it at the default log level; running with -v or -d adds the line Override ignored for property "build.dir", which is the only trace. The last two echo lines show the escape rule (next section) and an undefined property left as literal text.

Command-line properties always win

A property given with -D on the command line, or loaded with -propertyfile, is set before the build file is parsed, so no property task in the file can change it. The same happens if the arguments come from the ANT_ARGS environment variable. Run the same build as ant -Dbuild.dir=cli-wins:

Buildfile: /work/properties/ex1/build.xml

show:
     [echo] build.dir   = cli-wins
     [echo] classes.dir = cli-wins/classes
     [echo] ${build.dir} is cli-wins
     [echo] ${not.defined} stays: ${not.defined}

BUILD SUCCESSFUL
Total time: 0 seconds

With -propertyfile ci.properties (containing build.dir=from-file and classes.dir=file-classes) together with -Dbuild.dir=from-cli, the output was build.dir = from-cli and classes.dir = file-classes: both sources beat the build file, and -D beats -propertyfile, exactly as the usage text promises ("load all properties from file with -D properties taking precedence"). Setting ANT_ARGS="-Dbuild.dir=from-ant-args" gave build.dir = from-ant-args.

This is by design: a build file states defaults, and the caller (a developer, a continuous integration job) overrides them. It is also the single most useful idiom in Ant: write property for every tunable value and let people pass -D.

Where a value comes from: the order of precedence

flowchart TD A["Command line: -D, -propertyfile, ANT_ARGS"] --> B["Set before the build file is read: never overridden"] B --> C["Top-level property tasks in file order"] C --> D["Properties set inside targets in run order"] D --> E["local task shadows a name inside one scope"] C -. "first set wins" .-> F["Later definitions of the same name are ignored"] D -. "first set wins" .-> F

Two details complete the picture:

  • Top-level tasks run first. Since Ant 1.6 every project has an implicit target that holds all tasks written directly under <project>. It runs as part of initialisation, before any named target and even for ant -p. That is why the customary properties block at the top of a build file works, and why properties file loading at the top acts as "defaults" that every target sees.

  • Order of definition is the only way to prioritise. To let a per-user file override the project’s defaults, load the user’s file first. The next example does exactly that.

Property expansion

${name} and the $$ escape

In its default configuration Ant expands ${name} to the value of the property name. If no property of that name exists, the text is left untouched (as the last echo above shows) and nothing is reported. A typo in a property name is therefore a silent bug.

To write a literal ${...}, double the dollar sign: $${name} expands to ${name} and suppresses expansion of the text after it. The manual’s own example is a diagnostic message such as <echo>$${builddir}=${builddir}</echo>, which prints ${builddir}=build/classes. A lone $ that is not part of a property-like construct is kept literally for backward compatibility, but the manual advises always escaping it, and warns that mixing the two styles gives unpredictable results ($$$ yields $$).

Expansion happens when the property is set

The value of a property task is expanded at the moment the task executes. A property that is defined later is not seen:

<target name="c">
  <property name="x" value="${y}"/>
  <property name="y" value="late"/>
  <echo>x = ${x}</echo>
</target>
c:
     [echo] x = ${y}

Properties loaded from a file are different: Ant resolves references between the entries of one file regardless of their order. In the example below, tide.url refers to tide.host and tide.port, which are defined after it, and still expands correctly.

No nested expansion

The default expander finds the name between ${ and the first } and does not balance braces, so ${${which}} cannot look up a property whose name is held in another property. With which set to nested:

e:
     [echo] ${${which}}

No error is raised; the text simply stays as written. The Props Antlib (below) installs an expander that supports nesting.

Syntax errors

An unclosed reference is one of the few expansion problems that fails the build:

BUILD FAILED
/work/properties/ex7/build.xml:6: Syntax error in property: ${unclosed

The property task

The property task sets one property or many. The manual lists seven ways, and recommends using only one per task because combining them depends on evaluation order:

Form Effect

name + value, or nested text

Sets name to the value. <property name="motto">fair winds</property> is the same as the attribute form.

name + location

Sets name to the absolute path of the file, resolved against the project’s basedir (an absolute path is kept as it is, with / and \ converted to the platform convention).

name + refid

Copies a reference defined elsewhere. Reasonable only for path-like structures and other properties.

file

Loads a java.util.Properties file (ISO-8859-1, \uXXXX escapes for other characters). A missing file is not an error: nothing is printed except at -verbose, which lets projects have optional per-user files.

url

Loads properties-format text from a URL.

resource

Loads a properties file from the classpath (classpath attribute or nested classpath element, or classpathref).

environment

Defines one property per environment variable, prefixed with the given name and a dot.

runtime

Since 1.10.4: defines prefix.availableProcessors, prefix.freeMemory, prefix.totalMemory and prefix.maxMemory as snapshots taken when the task runs.

The remaining attributes modify how the values are produced:

Attribute Meaning

prefix

Prepended to every property loaded from file, resource or url; a trailing dot is added if missing.

prefixValues

Since 1.8.2. Whether the prefix is also applied when expanding references on the right-hand side of the loaded entries. Default false for property, but true for loadproperties, a difference the manual calls out.

relative, basedir

Since 1.8.0. With location, store the path relative to basedir (default: the project’s base directory) instead of an absolute one.

classpath, classpathref

Where to look for a resource.

From Ant 1.8.0, file, url and resource can also read the XML properties format when the name ends in .xml. Nested properties inside the loaded file are expanded when they are loaded.

A tour

The following build file uses every common form. It sits beside a conf/ directory:

conf/tidepool.properties
# defaults shared by all modules
tide.station=ES-0001
tide.datum=MSL
tide.url=https://${tide.host}:${tide.port}/api
tide.host=localhost
tide.port=8443
conf/override.properties
tide.station=ES-9999
conf/db.properties
db.host=localhost
db.url=jdbc:h2:tcp://${db.host}/tide
build.xml
<project name="props-tour" default="tour" basedir=".">
  <property name="greeting" value="ahoy"/>
  <property name="motto">fair winds</property>
  <property name="data.csv" location="data/stations.csv"/>
  <path id="cp">
    <pathelement location="lib/a.jar"/>
    <pathelement location="lib/b.jar"/>
  </path>
  <property name="cp.copy" refid="cp"/>

  <!-- the first definition wins: read the overrides before the defaults -->
  <property file="conf/override.properties"/>
  <property file="conf/tidepool.properties"/>

  <property file="conf/db.properties" prefix="plain"/>
  <property file="conf/db.properties" prefix="pv" prefixValues="true"/>

  <property name="rel" location="conf/tidepool.properties" relative="true" basedir="conf"/>

  <target name="tour">
    <echo>greeting     = ${greeting}</echo>
    <echo>motto        = ${motto}</echo>
    <echo>data.csv     = ${data.csv}</echo>
    <echo>cp.copy      = ${cp.copy}</echo>
    <echo>tide.station = ${tide.station}</echo>
    <echo>tide.url     = ${tide.url}</echo>
    <echo>plain.db.url = ${plain.db.url}</echo>
    <echo>pv.db.url    = ${pv.db.url}</echo>
    <echo>rel          = ${rel}</echo>
  </target>
</project>
Buildfile: /work/properties/ex2/build.xml

tour:
     [echo] greeting     = ahoy
     [echo] motto        = fair winds
     [echo] data.csv     = /work/properties/ex2/data/stations.csv
     [echo] cp.copy      = /work/properties/ex2/lib/a.jar:/work/properties/ex2/lib/b.jar
     [echo] tide.station = ES-9999
     [echo] tide.url     = https://localhost:8443/api
     [echo] plain.db.url = jdbc:h2:tcp://${db.host}/tide
     [echo] pv.db.url    = jdbc:h2:tcp://localhost/tide
     [echo] rel          = tidepool.properties

BUILD SUCCESSFUL
Total time: 0 seconds

What the output proves:

  • tide.station is ES-9999: the override file was read first, so it froze the value, and the default in tidepool.properties was ignored.

  • tide.url expanded although tide.host is defined after it in the same file.

  • plain.db.url kept the unresolved ${db.host}: the file was loaded with prefix="plain", so its entries became plain.db.host and plain.db.url, and the reference to db.host found nothing. prefixValues="true" makes the right-hand side use the prefix too, so pv.db.url resolved.

  • A path copied with refid becomes a single string using the platform’s path separator.

The environment attribute works the same way. After <property environment="env"/> the variable HOME is available as ${env.HOME} (the run on this machine printed the home directory). Names are case-sensitive even where the operating system is not: on Windows the variable Path appears as env.Path, not env.PATH. Since Ant 1.8.2 on Java 5 and later Ant reads System.getenv(); the old trick of running a platform command to list variables remains only for OpenVMS and as a fallback. The manual’s classic layered-configuration idiom is worth copying: read the user’s file, then the environment-specific file, then the project file, so the most specific definition wins:

<property environment="env"/>
<property file="${user.name}.properties"/>
<property file="${env.STAGE}.properties"/>
<property file="build.properties"/>

Built-in properties

Besides the JVM system properties, Ant defines a handful of properties of its own. The run below printed all of them for a project named tidepool-model, invoked as ant builtins refs.

Property Value

basedir

Absolute path of the project’s base directory (the basedir attribute of <project>).

ant.file

Absolute path of the build file.

ant.version

The version string of Ant.

ant.project.name

The name of the project currently executing.

ant.project.default-target

The default target of the project currently executing.

ant.project.invoked-targets

Comma-separated list of the targets requested on the command line, by an IDE or by an ant task. Set when the first target is executed; in the implicit target it is empty unless targets were requested.

ant.java.version

The Java version Ant detected.

ant.core.lib

Absolute path of ant.jar.

ant.home

Ant’s home directory. Set by the launcher script, so it can be missing inside an IDE.

ant.library.dir

The directory Ant’s jars were loaded from, normally ANT_HOME/lib. Set only when Ant is started through the Launcher class, so it may be missing inside an IDE.

Buildfile: /work/properties/ex3/build.xml

builtins:
     [echo] basedir                     = /work/properties/ex3
     [echo] ant.file                    = /work/properties/ex3/build.xml
     [echo] ant.version                 = Apache Ant(TM) version 1.10.18 compiled on September 3 2026
     [echo] ant.project.name            = tidepool-model
     [echo] ant.project.default-target  = builtins
     [echo] ant.project.invoked-targets = builtins,refs
     [echo] ant.java.version            = 26
     [echo] java.version                = 26
     [echo] ant.core.lib                = /opt/apache-ant-1.10.18/lib/ant.jar
     [echo] ant.home                    = /opt/apache-ant-1.10.18
     [echo] ant.library.dir             = /opt/apache-ant-1.10.18/lib

Note that ant.java.version is 26 here. The manual’s list of possible values ends at 9 and 1.8 and is out of date in that respect: since Ant 1.10.0 the property holds 9 rather than 1.9 on Java 9, and on later JDKs it is simply the major version. Compare with ant.project.invoked-targets: invoking ant with no argument gives builtins (the default target), and invoking ant refs builtins gives refs,builtins, in command-line order.

Some other "magic" properties are read by particular tasks and are documented with them:

Property Since Effect

ant.build.javac.source, ant.build.javac.target

1.7

Default source/target for javac (and javadoc for source) when the attributes are not set; see below.

ant.tmpdir

1.10.8

Directory for temporary files; see Running Ant.

ant.tstamp.now, ant.tstamp.now.iso

1.10.2

Fix the time tstamp uses; since 1.10.18 they also set the header timestamp written by propertyfile.

ant.disable.graal.nashorn.compat

1.10.9

Turns off the Nashorn compatibility mode Ant enables for GraalVM JavaScript; see Scripting.

Local properties

Since Ant 1.8 the local task creates a property whose scope is the current target (or the body of a sequential, parallel branch or macrodef). It shadows a global property of the same name from that point on, and is gone when the scope ends. The task only declares the name; the property is then set by an ordinary property task, which obeys the same "first wins" rule inside the scope.

<project name="scopes" default="all" basedir=".">
  <property name="mode" value="release"/>

  <target name="step1">
    <echo>before local: mode is ${mode}</echo>
    <local name="mode"/>
    <property name="mode" value="debug"/>
    <echo>after local:  mode is ${mode}</echo>
  </target>

  <target name="step2" depends="step1">
    <echo>in step2:     mode is ${mode}</echo>
  </target>

  <target name="mutate">
    <property name="mode" value="changed"/>
    <echo>after a second property task: mode is still ${mode}</echo>
  </target>

  <target name="child">
    <ant antfile="child.xml" inheritAll="false">
      <property name="mode" value="passed explicitly"/>
    </ant>
    <echo>from.child in the parent: ${from.child}</echo>
  </target>

  <target name="all" depends="step2,mutate,child"/>
</project>

child.xml echoes mode, then sets from.child:

<project name="child" default="run" basedir=".">
  <target name="run">
    <echo>child sees mode = ${mode}</echo>
    <property name="from.child" value="set in the child"/>
  </target>
</project>
Buildfile: /work/properties/ex4/build.xml

step1:
     [echo] before local: mode is release
     [echo] after local:  mode is debug

step2:
     [echo] in step2:     mode is release

mutate:
     [echo] after a second property task: mode is still release

child:

run:
     [echo] child sees mode = passed explicitly
     [echo] from.child in the parent: ${from.child}

all:

BUILD SUCCESSFUL
Total time: 0 seconds

Four lessons in one run: local changed mode inside step1 only; a plain second property task cannot change a global value; with inheritAll="false" a child build sees only the properties passed as nested property elements (by default inheritAll is true and all properties are passed down); and nothing a child sets travels back to the parent.

The place local pays for itself is the macro. Without it, a temporary property inside a macrodef is global and is frozen by the first invocation, so every later call reuses the first call’s value. The common workaround before 1.8 was to put an attribute into the name (parent.@{file}), which pollutes the global namespace. Declare it local instead (see Macros & Presets):

<macrodef name="makeparentdir">
  <attribute name="file"/>
  <sequential>
    <local name="parent"/>
    <dirname property="parent" file="@{file}"/>
    <mkdir dir="${parent}"/>
  </sequential>
</macrodef>

Since Ant 1.10.13 several names can be declared in one task with nested <name> elements. Since 1.10.9 a propertyset also sees in-scope local properties.

if and unless

Targets, and some tasks (fail, the test element of junit, the if: and unless: namespaces covered on Targets & Extension Points), take if and unless attributes. Their meaning changed in Ant 1.8.0:

  • Before 1.8.0 the value was always a property name, and the item was enabled if that property was defined, even as false or empty.

  • From 1.8.0 the value is first expanded: true, on or yes enables, false, off or no disables, and any other string is still treated as a property name. unless is the inverse.

Because the value is expanded, the condition can be overridden from the command line, which the old form could only do positively.

<project name="conditional" default="all" basedir=".">
  <target name="check-csv">
    <available property="csv.present" type="file" file="data/stations.csv"/>
  </target>

  <target name="validate" depends="check-csv" if="${csv.present}">
    <echo>validating data/stations.csv</echo>
  </target>

  <target name="skippable" unless="${skip.extras}">
    <echo>extras run</echo>
  </target>

  <target name="old-style" if="skip.extras">
    <echo>old style: runs when the property is merely defined, even as false</echo>
  </target>

  <target name="all" depends="validate,skippable,old-style"/>
</project>

With no CSV file and no skip.extras, running ant printed:

check-csv:

validate:

skippable:
     [echo] extras run

old-style:

all:

After creating data/stations.csv and running ant -Dskip.extras=false, validate, skippable and old-style all ran (the last one because the old form only asks whether the property exists). With -Dskip.extras=true, skippable was skipped, yet old-style still ran, which is the trap of mixing the two styles. A skipped target still prints its header line without any task output; a target skipped by if or unless does not skip the targets it depends on.

Reading properties from files and resources

loadproperties

loadproperties is the property file|resource task with two differences: it accepts a nested filterchain (see Filters & Mappers) and nested resources, and it fails the build when the source is missing, where property only warns at verbose level. Its attributes are srcFile, resource, encoding, classpath/classpathref, prefix (since 1.8.1) and prefixValues (since 1.8.2, default true, unlike property).

The tidepool build keeps its release settings in release.properties:

# tidepool release settings
import.csv=data/stations.csv
import.datum=MSL
export.dir=build/export
version.major=1
version.minor=4

To import only the keys that start with import.:

<loadproperties srcFile="release.properties">
  <filterchain>
    <linecontains>
      <contains value="import."/>
    </linecontains>
  </filterchain>
</loadproperties>

The missing-file behaviour, with a srcFile that does not exist, is a hard failure:

BUILD FAILED
/work/properties/ex7/build.xml:3: Source resource does not exist: /work/properties/ex7/missing.properties

loadfile and loadresource

loadfile reads a whole text file into one property; loadresource does the same for any resource (a URL, a file in a zip, a classpath entry), and loadfile is the file-only shorthand. If the content is empty after filtering, the property is not set. Both support encoding (the default is the platform encoding, so set it explicitly for portable builds), failonerror (default true), quiet (since 1.7.0; implies failonerror="false") and nested filterchain. Because properties are immutable, neither overwrites an existing property.

<loadfile property="banner" srcFile="banner.txt">
  <filterchain>
    <headfilter lines="1"/>
  </filterchain>
</loadfile>

xmlproperty

xmlproperty flattens a well-formed XML file into dotted property names, with the root element as the first segment (keepRoot, default true). Attributes become name(attribute) unless collapseAttributes="true". Plain xmlproperty does not expand ${...} references; set semanticAttributes="true" for that and for the special attributes id, location, refid, value and pathid. Other attributes are prefix, validate, includeSemanticAttribute, rootDirectory and delimiter (since 1.7.1, default ,).

With stations.xml:

<stations source="harbour-office">
  <station id="ES-0001">
    <name>Vigo</name>
    <constituent name="M2" amplitude="1.12"/>
  </station>
</stations>

this build file target loads all three sources (the full target is load in the example’s build.xml):

<xmlproperty file="stations.xml" collapseAttributes="true"/>
<echo>stations.source           = ${stations.source}</echo>
<echo>stations.station.id       = ${stations.station.id}</echo>
<echo>stations.station.name     = ${stations.station.name}</echo>
<echo>constituent.amplitude     = ${stations.station.constituent.amplitude}</echo>
load:
     [echo] import.csv   = data/stations.csv
     [echo] export.dir   = ${export.dir}
     [echo] banner       = tidepool
     [echo] stations.source           = harbour-office
     [echo] stations.station.id       = ES-0001
     [echo] stations.station.name     = Vigo
     [echo] constituent.amplitude     = 1.12

export.dir was filtered out by the linecontains filter, so its reference stays literal; banner holds only the first line of banner.txt.

Writing and inspecting properties

propertyfile

propertyfile edits a properties file in place. It is an optional task, built for "bump the build number" steps. Nested entry elements describe each change: key (required), value, default, type (string, int or date), operation (= default, +, - for int and date, or del), pattern for formatting numbers and dates and unit (millisecond through year, default day) for date arithmetic. The task header attributes are file, comment and jdkproperties. Since 1.8.0 it preserves the comments and layout of the original file (set jdkproperties="true" to get the old behaviour back, which discards them) and since 1.8.2 it preserves the line-ending style. Since 1.10.18 the timestamp comment honours SOURCE_DATE_EPOCH, ant.tstamp.now and ant.tstamp.now.iso (only when jdkproperties is false), so reproducible builds stop getting a new timestamp on every run.

<propertyfile file="release.properties">
  <entry key="version.minor" type="int" operation="+" value="1"/>
  <entry key="version.patch" type="int" default="0"/>
</propertyfile>
<concat><file file="release.properties"/></concat>

Run as SOURCE_DATE_EPOCH=1767225600 ant bump against the file shown earlier:

bump:
Honouring environment variable SOURCE_DATE_EPOCH which has been set to 1767225600
[propertyfile] Updating property file: /work/properties/ex6/release.properties
   [concat] #Thu, 01 Jan 2026 00:00:00 +0000
   [concat] # tidepool release settings
   [concat] import.csv=data/stations.csv
   [concat] import.datum=MSL
   [concat] export.dir=build/export
   [concat] version.major=1
   [concat] version.minor=5
   [concat]
   [concat] version.patch=0

The comments survived, version.minor went from 4 to 5, and version.patch was added with its default because it did not exist. The task wrote a timestamp comment first (here the fixed date, as requested) and, as this run shows, a blank line before the new key; the file is still a valid properties file.

echoproperties

echoproperties prints all properties, or a subset, to the log or to destfile. It is meant for debugging a build file. Attributes: destfile, prefix, regex, failonerror (default true) and format (text or xml); the properties can also be selected with a nested propertyset. Since 1.10.14 the output is sorted on JDK 9 and later.

propertyset

propertyset (since 1.6) groups properties so a task can take them as a block: for example to give a selection to echoproperties or, as a syspropertyset, to a forked java. Attributes are dynamic (re-evaluate on every use, default true), negate (since 1.6.2) and refid. Nested propertyref elements select by name, prefix, regex or builtin (all, system, commandline); nested propertyset elements form a union, and at most one mapper renames keys.

<property name="tide.a" value="1"/>
<property name="tide.b" value="2"/>
<propertyset id="tide-props">
  <propertyref prefix="tide."/>
  <mapper type="glob" from="tide.*" to="cfg.*"/>
</propertyset>
<echoproperties prefix="tide."/>
<echo>${toString:tide-props}</echo>
<echoproperties destfile="build.props.out" prefix="tide."/>
<concat><file file="build.props.out"/></concat>
dump:
[echoproperties] #Ant properties
[echoproperties] #Fri Oct 16 11:36:54 CEST 2026
[echoproperties] tide.a=1
[echoproperties] tide.b=2
     [echo] cfg.a=1, cfg.b=2
   [concat] #Ant properties
   [concat] #Fri Oct 16 11:36:54 CEST 2026
   [concat] tide.a=1
   [concat] tide.b=2

The file written by echoproperties is a normal properties file, so a later property file= can read it back, which is the "return values from a child build" trick the manual mentions as a contrived use.

References as strings: toString: and ant.refid:

Two built-in prefixes read a project reference instead of a property.

  • ${toString:id} expands to the toString() of the referenced object. The manual’s example lists a fileset’s files; the run on a small tree printed a/One.java;a/Two.java for ${toString:sources}; a fileset always joins its entries with ;, whatever the platform, while a path uses the platform path separator (: on macOS and Linux). For a third-party type there is no guarantee that toString() is meaningful.

  • ${ant.refid:id} expands to the referenced object itself, which matters only for tasks whose attribute setters accept a type other than String (a Resource, for instance). Surrounded by other text it behaves like toString:.

<fileset id="sources" dir="src" includes="**/*.java"/>
<echo>toString: ${toString:sources}</echo>
refs:
     [echo] toString: a/One.java;a/Two.java

PropertyHelper and its delegates

Behind every ${...} is one org.apache.tools.ant.PropertyHelper instance attached to the project. Since Ant 1.8 it is a thin coordinator that consults a chain of delegates, which makes property handling pluggable:

Delegate interface Responsibility

PropertyExpander

Finds a property name inside a string. The default one extracts foo from ${foo} and does not balance braces.

PropertyHelper.PropertyEvaluator

Turns a name into an object. toString: and ant.refid: are evaluators, and so is the storage of local properties.

PropertyHelper.PropertySetter

Writes a property; the other half of the storage of local properties.

PropertyHelper.PropertyEnumerator

Since 1.10.9: lists the property names a delegate manages, so tasks like propertyset and echoproperties can see properties kept outside the project.

The propertyhelper task (since 1.8.0) installs a delegate or a whole replacement helper at run time. Delegates are consulted in last-in, first-out order, so the one added last runs before the built-in ones. This is advanced usage that needs Java code; the manual’s example registers an evaluator class with componentdef and then wraps it in <propertyhelper>. Not run here, because it needs a compiled delegate class; a worked custom class belongs to Writing Custom Tasks.

The Props Antlib

The Apache Props Antlib is a library of ready-made delegates. It provides nested (a PropertyExpander that supports ${${double-expand-me}}), stringops (shell-like string operations), types (a type-aware evaluator) and others, and is installed with propertyhelper. Install a Props Antlib release into one of Ant’s library locations (see Antlibs & Namespaces). Not run here: the Antlib is a separate download that the section’s fixed set of tools does not include. In practice most builds never need it; if you need computed property names, a macrodef or the script task is usually simpler to maintain.

The ant.build.javac.* properties

javac has no default source and target; the compiler’s defaults depend on the JDK that runs the build, so a build file that omits them can stop compiling when the JDK is upgraded. When you cannot edit the file, two properties supply the defaults, and are ignored when the attributes are set explicitly: ant.build.javac.source (since 1.7; also used by javadoc) and ant.build.javac.target (since 1.7).

Given a one-line Height record and a build file with a bare javac task and no source or target, the run on JDK 26 with ant -Dant.build.javac.source=17 -Dant.build.javac.target=17 compile printed:

compile:
    [mkdir] Created dir: /work/properties/ex8/build/classes
    [javac] Compiling 1 source file to /work/properties/ex8/build/classes
    [javac] warning: [options] location of system modules is not set in conjunction with -source 17
    [javac]   not setting the location of system modules may lead to class files that cannot run on JDK 17
    [javac]     --release 17 is recommended instead of -source 17 -target 17 because it sets the location of system modules automatically
    [javac] 1 warning
     [echo] ant.build.javac.source = 17
     [echo] ant.build.javac.target = 17

BUILD SUCCESSFUL

javap confirmed class file major version 61 (Java 17). The compiler’s own warning is the lesson: since Ant 1.9.8 javac has a release attribute that passes --release, and it is the better choice on JDK 9 and later because it also selects the matching platform API. Use these two properties only as a stop-gap and put release in the build file (see Compiling Java). Without the properties and without the attributes, a verbose run (-v) shows the property lookup failing with Property "ant.build.javac.source" has not been set.

Classic pitfalls

  • "My setting is ignored." Something defined the same name earlier: another property, a -D, an imported build file that runs before yours, or a property file loaded above your task. Run with -v and look for Override ignored for property "name". The fix is to order the definitions, not to repeat the task.

  • "My ${...} is printed literally." The name is misspelt, the property is defined later, or the property is defined only in a local scope that has ended. Ant never reports it. A defensive build uses fail with unless for required properties:

    <fail message="Set -Dtide.station=... or add it to conf/tidepool.properties" unless="tide.station"/>
  • Properties as loop variables. Because they are immutable, a property cannot be a counter. Use a macro with local, a loop from the third-party Ant-Contrib library, or the script task; do not look for an "assign" task in core Ant.

  • antcall and ant do not return values. A child build cannot set a property for its caller. Use an echoproperties file, a shared properties file, or restructure with depends, import and macros.

  • Environment name case. On Windows the variable Path is env.Path; scripts written on Linux with env.PATH find nothing.

  • Encoding. Properties files are read as ISO-8859-1; write other characters as \uXXXX, or load the file with loadproperties and an explicit encoding.

  • Trailing spaces. They are part of the value in a properties file, as in java.util.Properties.

  • Missing optional file. property file= is silent about a missing file (intentionally); loadproperties fails. Choose according to whether the file is optional.

  • Syntax error at runtime. An unclosed reference stops the build with Syntax error in property: ${b; the error names the line of the build file.

Where older Ant material says that top-level code may contain only property, typedef and taskdef, or describes ant.java.version as one of 1.2 to 1.9, it is out of date: since Ant 1.6 every task may appear outside a target, and the property now holds the JDK’s own major version.

Properties in Maven and Gradle

Concept Ant Maven Gradle

Define a value

<property name= value=/>, immutable

<properties> in the POM, ${name}

val/ext, gradle.properties, -P

Override from the command line

-Dname=value, -propertyfile

-Dname=value

-Pname=value, -Dname=value, ORG_GRADLE_PROJECT_name

Environment

<property environment="env"/>, ${env.X}

${env.X}

System.getenv() or a provider

Scoped value

<local>

a plugin parameter or profile

a task or project property

See POM Fundamentals and Profiles for Maven properties, and Build Environment & Properties for Gradle’s. The Gradle build can also call Ant tasks and read Ant properties; see Ant Integration.

Where to go next