Schmant User's Guide

Revision History
Revision 1.0.12010.02.16

Table of Contents

1. Introduction
This manual
Learning Schmant
Please help!
License
2. Installing Schmant
Requirements
Downloading
Installing
Running unit tests (optional)
Running unit tests on Unix
Running unit tests on Windows
3. Running Schmant
Unix
Arguments
Environment variables
Which Java? (Unix)
Windows
Arguments
Environment variables
Which Java? (Windows)
Running Schmant from Java
4. Build scripts and tasks
A real-world example
Tasks and task factories
Interpreted arguments
Future entities
Producers
Recursive tasks
5. Environment and resources
Reports and logging
Schmant and EntityFS
Properties and arguments
Utility classes
Temporary files and directories
Including other script files
6. Task packages
7. Task executors
Closures
Dependencies for tasks with subtasks
Task executors and future entities
8. Projects
Building with the workspace builder task
Building manually
Eclipse workspaces
IntelliJ IDEA projects
9. Advanced topics
Class loaders
In-memory file systems
10. Best practices
Build scripts are programs
Use EntityFS file systems
Set a temporary directory
11. Troubleshooting
Increase the report level for the failing task
Trace log failing task configuration
Setting the number of build threads to one
Keeping temporary files
Using a remote debugger
A. ArgumentInterpreter
Argument philosophy
Interpretation process
java.io.File (file or directory)
java.io.File (directory)
java.net.URL
FutureEntity
NamedReadableFile
ReadableFile
Existing WritableFile
New WritableFile
RandomlyAccessibleFile
Read only entity holder
Read only Entity (file or directory)
Read only Directory
Read/write Directory
Read/write Entity (file or directory)
Read/write EFile
XML Source
ReadableFile implementations
NamedReadableFile implementations
WritableFile implementations
RandomlyAccessibleFile implementations
EntityView implementations
FutureEntityStrategy implementations
B. EntityFS cookbook
EntityFS overview
Interoperability with java.io classes
File systems
Entities
Directory views
Utility classes
EntityFilter implementations
Best practices
Bibliography

List of Figures

A.1. EntityFS EFile inheritance hierarchy (subset)

List of Tables

4.1. Target strategies
5.1. Report logging levels
8.1. ProjectFilter implementations
9.1. Build times for the Schmant workspace
9.2. Build and preprocess times for the Schmant workspace
A.1. Interpretation of an argument into a file or directory java.io.File
A.2. Interpretation of an argument into a java.net.URL
A.3. Interpretation of an argument into a FutureEntity
A.4. Interpretation of an argument into a NamedReadableFile
A.5. Interpretation of an argument into a ReadableFile
A.6. Interpretation of an argument into an existing WritableFile
A.7. Interpretation of an argument into a new WritableFile
A.8. Interpretation of an argument into a RandomlyAccessibleFile
A.9. Interpretation of an argument into an entity holder
A.10. Interpretation of an argument into a read only EntityView
A.11. Interpretation of an argument into a read only Directory
A.12. Interpretation of an argument into a read/write Directory
A.13. Interpretation of an argument into a read/write Entity
A.14. Interpretation of an argument into a read/write EFile
A.15. Interpretation of an argument into an XML Source
B.1. FileSystemBuilder implementations
B.2. EntityFS utility classes (useful subset)
B.3. EntityFilter implementations

List of Examples

4.1. A Hello World script
4.2. Compile Java files and build a Jar file
4.3. Using different source types with CopyTF
4.4. Flattening an argument list
4.5. Using a produced file as the source in another task.
4.6. Set version information in all XML files in a directory hierarchy, overwriting the original files, using a recursive iterator.
4.7. Set version information in all XML files in a directory hierarchy, overwriting the original files, using a RecursiveActionTF
4.8. Set version information in all XML files in a directory hierarchy, putting the target files in a new directory hierarchy, using a recursive iterator.
4.9. Set version information in all XML files in a directory hierarchy, putting the target files in a new directory hierarchy.
4.10. Set version information in all XML files in a directory hierarchy, putting the target files in a two new directory hierarchies.
5.1. Iterating over files and directories
5.2. Hiding .svn directories when creating an EclipseWorkspace
5.3. From Java files to EntityFS and back again
5.4. Using EntityFS property methods
5.5. Using JdkUtil
6.1. Enabling the Findbugs task package and running Findbugs.
7.1. Compile Java files and build a Jar file, using a task executor
7.2. Reusing a task factory
7.3. Running a closure with a task executor
7.4. Using a closure instead of a task factory
7.5. Dependencies for task with subtasks
7.6. Using future files to represent files that do not yet exist
7.7. Computing statistics for an XML file
8.1. Build all projects in an Eclipse workspace using the workspace builder task.
8.2. Build all projects in an Eclipse workspace manually
8.3. Build a Jar file from the projects in an Eclipse workspace
8.4. Build a Jar file from the projects in an Eclipse workspace manually
8.5. Build an Eclipse workspace using a user-defined library
8.6. Preprocess source files in Eclipse projects. Compile. Build Jar
8.7. Preprocess source files in Eclipse projects. Compile manually. Build Jar
8.8. Build an IntelliJ workspace
9.1. Creating an in-memory temporary directory
10.1. Creating file systems for the build script
B.1. Creating source and target file systems
B.2. Using the SchmantReportLogAdapter

Schmant is a build tool for building and packaging software applications and libraries. It provides an environment for running build scripts and tasks and tools that the scripts may use. Build scripts may be written in JavaScript, Groovy, JRuby or Jython. See the scripting language guides for details.

A Schmant build script has access to all API:s of the Java platform, any number of user-supplied classes, as well as the features of the script language used. It has also access to the file system APIs of EntityFS. This makes it possible to write very powerful and customizable build scripts.

For programmers that are new to Schmant, the amount of new concepts can sometimes feel daunting. In order to make the most of Schmant, it is important that you feel comfortable with:

As always, when writing scripts, first make sure that your script works before you try to make it work faster or be more generic.

Download Schmant from http://www.schmant.org. There are two distributions to choose between. The binary distribution includes everything that is needed to run Schmant and all documentation. The source distribution contains everything that the binary distribution contains, as well as the complete Schmant source code in an Eclipse workspace.

The source distribution comes with a set of unit tests. For running them, the following requirements must be met:

The unit tests will generate verbose output and what may seem to be a lot of errors. That is just part of the normal tests. The interesting output comes in the test report that is written when all tests have been run.

Schmant is started with the startup script schmant.sh or schmant.bat, depending on the operating system on which it is run. The startup script searches for a java command using the algorithm described in the section called “Which Java? (Unix)” or the section called “Which Java? (Windows)”, then it uses that java command to launch the Launcher class. Launcher prepares the scripting environment (see the scripting language guides) and invokes the build script.

Most script languages require that the script language implementation JARs are on the classpath. Either put them on the classpath manually, or put them in the Schmant installation's libengine directory.

A large number of different arguments can be given to the startup script. However, except for the script file, none of them are mandatory. The default settings of the startup script should work fine in many cases.

On Unix (Linux, Solaris, Mac OS X, etc.), Schmant is run with the bin/schmant.sh command.

[ -a scripts | --prepare-scripts scripts ]

A list of custom environment preparation script files. The scripts are run in the order that they are listed. The default, script environment-specific preparation script can be included in the list by inserting a capital X in any position. If this argument is omitted, only the default environment preparation script is run.

See the scripting language guides for more information on the different environment preparation scripts.

Different path entries are separated by colons (:). Path entries are either absolute or relative to the directory of the build script.

[{ -c | --class-loader-strategy } { isolated | shared }]

Choose the class loading strategy for task packages. The default strategy is shared. See the section called “Class loaders”.

[ -cp path | --classpath path ]

Insert this classpath first in the script classpath. Different path entries are separated by colons (:). Path entries are either absolute or relative to the directory of the build script.

[ -h | --help ]

Print usage instructions and exit.

[ -i fqn | --argument-interpreter fqn ]

The fully qualified class name of the ArgumentInterpreter implementation to use.

[ -j arg | --jvmarg arg ...]

Give an argument to the JVM used to run Schmant (for instance -j -Xmx512m to set the maximum available memory size to 512MB). Use several times to give several arguments.

If the argument contains spaces, = characters or other characters that the shell interpreter has special treatment for, it has to be quoted in double (") or single (') quotes.

[ --java path ]

Use the specified Java installation to run Schmant. The path should be the path to the root directory of the Java installation, for instance --java /opt/jdk1.6.0_12.

[ -k | --keep ]

Keep all temporary files and directories created with TempFileUtil. If this flag is not given, all temporary files and directories that the build script has not explicitly told TempFileUtil to keep are deleted when Schmant exits.

[ -l | --list-executor-factories ]

List all available org.schmant.ScriptExecutorFactory implementations and exit.

[ -p key=value | --property key=value ...]

Set a script property. The property will be available for the build script in the props Properties variable. Give the argument several times to set several properties. See the section called “Properties and arguments”.

[ -q | --quiet ]

Decrease the level of verbosity. (Make Schmant print less information about what it is doing.) Use the flag several times to make Schmant even more quiet. See the section called “Reports and logging”.

[ -r fqn | --report-factory fqn ]

The fully qualified class name of the ReportFactory to be used for creating the build reports. Default is the StdoutReportFactory. See the section called “Reports and logging”.

[ -rlf fqn | --report-line-formatter fqn ]

The fully qualified class name of the ReportLineFormatter to be used for formatting text lines that are logged to a Report. Default is the SimpleReportLineFormatter. See the section called “Reports and logging”.

[ --script-executor-implementation fqn ]

The fully qualified class name for the ScriptExecutor implementation to use for running the build script. If this argument is not given, a ScriptExecutor is looked up using the script file extension.

[ -t path | --taskpath path ]

Set the path to additional task packages, in addition to the task packages bundled with Schmant. Task packages may be either task package Zip files, or the root directory of an unpacked Zip file, just like Jar files and class directories are used in the Java classpath.

Different path entries are separated by colons (:). Path entries are either absolute or relative to the directory of the build script.

Extra task package path entries are prepended to the task package path string, and thus take precedence over task directories in the standard task package path. (The $SCHMANT_HOME/task directory.) See Chapter 6, Task packages.

[ --trace ]

Enable trace mode from the start. Trace mode means that a task's configuration always is logged before it is run.

[ -v | --verbose ]

Increase the level of verbosity. (Make Schmant print more information about what it is doing.) Use the flag several times to make Schmant even more verbose. See the section called “Reports and logging”.

file

The path to the script file to run. To run a script stored in a Zip, Jar, War or Ear file, use the format path_to_archive#path_to_script_in_archive.

arg...

A list of script arguments. All arguments given here are bound to the List<String> args in the script.

JAVA_HOME

If set, the JDK installation referenced will be used to run Schmant.

JVM_ARGS

Can be used instead of or together with the -j argument to set JVM options.

PROG_ARGS

Arguments that are fed directly to the Launcher class, the class used to launch Schmant.

TASK_PACKAGE_PATH

Task package path that is prepended to the default task package path. The default task package path contains all task packages that are bundled with Schmant. This is ignored if the -t argument is used.

Some tasks use other environment variables in addition to those listed above.

On Windows, Schmant is run with the bin\schmant.bat command.

[ -a scripts | --prepare-scripts scripts ]

A list of custom environment preparation script files. The scripts are run in the order that they are listed. The default, script environment-specific preparation script can be included in the list by inserting a capital X in any position. If this argument is omitted, only the default environment preparation script is run.

See the scripting language guides for more information on environment preparation scripts.

Different path entries are separated by semi colons (;). Path entries are either absolute or relative to the directory of the build script.

[{ -c | --class-loader-strategy } { isolated | shared }]

Choose the class loading strategy for task packages. The default strategy is shared. See the section called “Class loaders”.

[ -cp path | --classpath path ]

Insert this classpath first in the script classpath. Different path entries are separated by semi colons (;). Path entries are either absolute or relative to the directory of the build script.

If there are several entries on the classpath, or if the entry on the classpath contains spaces, the classpath argument (only the path part) has to be quoted in ":s

[ -h | --help ]

Print usage instructions and exit.

[ -i fqn | --argument-interpreter fqn ]

The fully qualified class name of the ArgumentInterpreter implementation to use.

[ -j arg | --jvmarg arg ...]

Give an argument to the JVM used to run Schmant (for instance -j -Xmx512m to set the maximum available memory size to 512MB).

If the argument contains spaces or equals signs (for instance when assigning a Java system property), enclose the argument in quotes (").

Use several times to give several arguments.

[ --java path ]

Use the specified Java installation to run Schmant. The path should be the path to the root directory of the Java installation. If it contains spaces, it must be quoted in citation marks, for instance --java "c:\Program Files\Java\jdk1.6.0_12".

[ -k | --keep ]

Keep all temporary files and directories created with TempFileUtil. If this flag is not given, all temporary files and directories that the build script has not explicitly told TempFileUtil to keep are deleted when Schmant exits.

[ -l | --list-executor-factories ]

List all available org.schmant.ScriptExecutorFactory implementations and exit.

[ -p "key=value" | --property "key=value" ...]

Set a script property. The property will be available for the build script in the props Properties variable. The property assignment must be enclosed in quotes. Give the argument several times to set several properties. See the section called “Properties and arguments”.

[ -q | --quiet ]

Decrease the level of verbosity. (Make Schmant print less information about what it is doing.) Use the flag several times to make Schmant even more quiet. See the section called “Reports and logging”.

[ -r fqn | --report-factory fqn ]

The fully qualified class name of the ReportFactory to use for creating the build reports. Default is the StdoutReportFactory. See the section called “Reports and logging”.

[ -rlf fqn | --report-line-formatter fqn ]

The fully qualified class name of the ReportLineFormatter to be used for formatting text lines that are logged to a Report. Default is the SimpleReportLineFormatter. See the section called “Reports and logging”.

[ --script-executor-implementation fqn ]

The fully qualified class name for the ScriptExecutor implementation to use for running the build script. If this argument is not given, a ScriptExecutor is looked up using the script file extension.

[ -t path | --taskpath path ]

Set the path to additional task packages, in addition to the task packages bundled with Schmant. Task packages may be either task package Zip files, or the root directory of an unpacked Zip file, just like Jar files and class directories are used in the Java classpath.

Different path entries are separated by semicolons (;). Path entries are either absolute or relative to the directory of the build script.

Extra task package path entries are prepended to the task package path string, and thus take precedence over task directories in the standard task package path. (The $SCHMANT_HOME/task directory.) See Chapter 6, Task packages.

[ --trace ]

Enable trace mode from the start. Trace mode means that a task's configuration always is logged before it is run.

[ -v | --verbose ]

Increase the level of verbosity. (Make Schmant print more information about what it is doing.) Use the flag several times to make Schmant even more verbose. See the section called “Reports and logging”.

file

The path to the script file to run. To run a script stored in a Zip, Jar, War or Ear file, use the format path_to_archive#path_to_script_in_archive.

arg...

A list of script arguments. All arguments given here are bound to the List<String> args in the script.

JAVA_HOME

If set, the JDK installation referenced will be used to run Schmant.

JVM_ARGS

Can be used instead of or together with the -j argument to set JVM options.

PROG_ARGS

Arguments that are fed directly to the Launcher class, the class used to launch Schmant.

TASK_PACKAGE_PATH

Task package path that is prepended to the default task package path. The default task package path contains all task packages that are bundled with Schmant. This is ignored if the -t argument is used.

Some tasks use other environment variables in addition to those listed above.

Schmant can be embedded in a Java application. The application runs Schmant's by creating a Launcher object and running any of its launch methods. See Launcher's API documentation for details on how to do that.

The build script is a script that binds together a sequence of tasks. The script can be written in any supported script language. See the scripting language guides.

The mandatory Hello World script looks like this when written in JavaScript:


The script in the example above uses the info method to log output to its current Report. Report objects are used for handling output from a build script and have logging methods much like any logging API. Read more on reports in the section called “Reports and logging”.

The next example shows a script that compiles the Java source files found in the directory src and its subdirectories, puts the compiled classes in a temporary directory and then bundles them in a Jar file.

Example 4.2. Compile Java files and build a Jar file

Groovy

import java.io.File import java.text.SimpleDateFormat import java.util.Date import org.entityfs.util.Directories import org.schmant.support.io.TempFileUtil import org.schmant.support.entityfs.SchmantFileSystems import org.schmant.task.jdk.jar.JarTF import org.schmant.task.jdk.javac.jdk6.Jdk6JavacTF // Compile files from the directory /home/me/myproject/src and put the resulting // class files in a temporary directory. The source files have compilation // dependencies to all Jar files in /home/me/myproject/lib and to the Jar file // /home/me/myproject/optlib/opt.jar // A temporary java.io.File directory for the compiled classes. This, along with // all of its contents, will be automatically deleted when Schmant exits (unless // the -k flag was used when Schmant was launched). def ctarget = TempFileUtil.createTempDir() // This is the lib directory expressed as a (read only) EntityFS Directory. // By using a Directory, the script can use the utility methods of // Directories to extract the files it wants. See below. def libDir = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/lib"), true) // Get all jar files from the lib directory. The jar files are returned as a // Set of EntityFS EFile:s def depjars = Directories.getAllFilesMatching(libDir, "*.jar") // Add a dependency from the optlib directory. This is a Java File depjars.add(new File("/home/me/myproject/optlib/opt.jar")) // Compile the Java source files. new Jdk6JavacTF(). addSource(new File("/home/me/myproject/src")). addClasspathEntries(depjars). setTarget(ctarget).run() // A timestamp for the built archive def timestamp = new SimpleDateFormat("yyyyMMddHHmm"). format(new Date()) // Build the Jar file. Put it in /home/me/myproject new JarTF(). addSource(ctarget). setTarget(new File("/home/me/myproject/myproject" + timestamp + ".jar")). run()

JavaScript

// Compile files from the directory /home/me/myproject/src and put the resulting // class files in a temporary directory. The source files have compilation // dependencies to all Jar files in /home/me/myproject/lib and to the Jar file // /home/me/myproject/optlib/opt.jar // A temporary java.io.File directory for the compiled classes. This, along with // all of its contents, will be automatically deleted when Schmant exits (unless // the -k flag was used when Schmant was launched). ctarget = TempFileUtil.createTempDir(); // This is the lib directory expressed as a (read only) EntityFS Directory. // By using a Directory, the script can use the utility methods of // Directories to extract the files it wants. See below. libDir = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/lib"), true); // Get all jar files from the lib directory. The jar files are returned as a // Set of EntityFS EFile:s depjars = Directories.getAllFilesMatching(libDir, "*.jar"); // Add a dependency from the optlib directory. This is a Java File depjars.add(new File("/home/me/myproject/optlib/opt.jar")); // Compile the Java source files. new Jdk6JavacTF(). addSource(new File("/home/me/myproject/src")). addClasspathEntries(depjars). setTarget(ctarget).run(); // A timestamp for the built archive // java.text.SimpleDateFormat must be fully qualified since the java.text // package classes are not automatically imported by Schmant. The "Packages" // prefix is for telling JavaScript that it is a Java package. It is required // sometimes. See the chapter on script language support for more information. timestamp = new Packages.java.text.SimpleDateFormat("yyyyMMddHHmm"). format(new Date()); // Build the Jar file. Put it in /home/me/myproject new JarTF(). addSource(ctarget). setTarget(new File("/home/me/myproject/myproject" + timestamp + ".jar")). run();

JRuby

# Compile files from the directory /home/me/myproject/src and put the resulting # class files in a temporary directory. The source files have compilation # dependencies to all Jar files in /home/me/myproject/lib and to the Jar file # /home/me/myproject/optlib/opt.jar # A temporary java.io.File directory for the compiled classes. This, along with # all of its contents, will be automatically deleted when Schmant exits (unless # the -k flag was used when Schmant was launched). ctarget = Schmant::TempFileUtil.createTempDir # This is the lib directory expressed as a (read only) EntityFS Directory. # By using a Directory, the script can use the utility methods of # Directories to extract the files it wants. See below. # # File is not included in the Java module, so we have to access it through the # Java module instead. libDir = Schmant::SchmantFileSystems.getEntityForDirectory( Java::JavaIo::File.new("/home/me/myproject/lib"), true) # Get all jar files from the lib directory. The jar files are returned as a # Set of EntityFS EFile:s depjars = Schmant::Directories.getAllFilesMatching(libDir, "*.jar") # Add a dependency from the optlib directory. This is a Java File depjars.add(Java::JavaIo::File.new("/home/me/myproject/optlib/opt.jar")) # Compile the Java source files. Schmant::Jdk6JavacTF.new. addSource(Java::JavaIo::File.new("/home/me/myproject/src")). addClasspathEntries(depjars). setTarget(ctarget).run # A timestamp for the built archive timestamp = Java::JavaText::SimpleDateFormat.new("yyyyMMddHHmm"). format(Java::JavaUtil::Date.new) # Build the Jar file. Put it in /home/me/myproject Schmant::JarTF.new. addSource(ctarget). setTarget( Java::JavaIo::File.new("/home/me/myproject/myproject" + timestamp + ".jar")). run

Jython

# Compile files from the directory /home/me/myproject/src and put the resulting # class files in a temporary directory. The source files have compilation # dependencies to all Jar files in /home/me/myproject/lib and to the Jar file # /home/me/myproject/optlib/opt.jar # A temporary java.io.File directory for the compiled classes. This, along with # all of its contents, will be automatically deleted when Schmant exits (unless # the -k flag was used when Schmant was launched). ctarget = TempFileUtil.createTempDir() # This is the lib directory expressed as a (read only) EntityFS Directory. # By using a Directory, the script can use the utility methods of # Directories to extract the files it wants. See below. libDir = SchmantFileSystems.getEntityForDirectory( File("/home/me/myproject/lib"), True) # Get all jar files from the lib directory. The jar files are returned as a # Set of EntityFS EFile:s depjars = Directories.getAllFilesMatching(libDir, "*.jar") # Add a dependency from the optlib directory. This is a Java File depjars.add(File("/home/me/myproject/optlib/opt.jar")) # Compile the Java source files. Jdk6JavacTF(). \ addSource(File("/home/me/myproject/src")). \ addClasspathEntries(depjars). \ setTarget(ctarget).run() # A timestamp for the built archive # # SimpleDateFormat is not automatically imported by the preparation # script. from java.text import SimpleDateFormat timestamp = SimpleDateFormat("yyyyMMddHHmm"). \ format(Date()) # Build the Jar file. Put it in /home/me/myproject JarTF(). \ addSource(ctarget). \ setTarget(File("/home/me/myproject/myproject" + timestamp + ".jar")). \ run()

There are two important things to point out in the example above:

  • "TF" in, for instance, Jdk6JavacTF stands for Task Factory. Task objects are never created directly, only via their task factories. Task factories may be reused for creating several task objects.
  • Both Java's File and EntityFS file and directory objects are used by the script. Tasks can handle both of them because they use interpreted arguments for some of their methods.

The following sections will explain each of the bullet points in more detail.

Task:s and TaskFactory:s are closely related. A task object is used just once for running a task and then it is discarded. A task factory is a factory object used for creating one or more task objects. It can be reused to create several tasks with similar configurations, for instance when processing files recursively in a directory hierarchy (see example Example 4.7, “Set version information in all XML files in a directory hierarchy, overwriting the original files, using a RecursiveActionTF”). Build scripts always use task factories to create tasks; tasks objects are never created directly.

To create a task, a build script creates a task factory and sets all of the task's configuration on the factory using its setter methods. Then the script creates one or several tasks from the factory and runs them. Behind the scenes, the task factory configures a TaskSpecification object, which it hands over to the Task object that it creates. After creating the task, if the script continues to configure the same task factory, the task factory makes a copy of the task specification object so that the configuration of the already created task does not change. After creating the next task, the task specification is copied again, and so on. This ensures that the configuration of a running task never is modified.

There are four different categories of tasks:

Action tasks

An action task factory has a configurable source property. It creates action tasks that read the data in the source property and perform some action with it. Examples: ChmodTF and DomParseXmlTF.

Generator tasks

A generator task factory has a configurable target property. It creates generator tasks that generates data that they write to the target. Example: SvnExportTF.

Process tasks

A process task factory has both a source and a target property. It creates process tasks that read data from the source, transforms it (compiles it, for instance), and write the result to the target. A process task is both an action task and a generator task. Examples: Jdk6JavacTF and TextReplaceTF.

Plain tasks

A plain task factory does not have source or target properties. Examples: JavaTF and CompoundTF.

The source and target properties are interpreted (see below), so the exact types of the properties varies from task to task. source is often a file or a directory and target is often a directory or a FutureFile (see below).

Both Task and TaskFactory implement java.lang.Runnable. Calling run() on a TaskFactory is a shortcut for calling create().run() on it.

The reason for separating the task and task factory functionality into two different classes is illustrated by Example 4.7, “Set version information in all XML files in a directory hierarchy, overwriting the original files, using a RecursiveActionTF” below. It shows how an action task is created and run for each XML file in a directory hierarchy. The task factory is a template for creating tasks that only differ in their source properties.

The Task factory index lists all available task factories.

Some task factory property setter methods take Object arguments. These arguments are interpreted, which means that it is up to the task to interpret them into something useful. This gives a great deal of flexibility in the different kinds of variables that a Schmant script may use. For instance, a Schmant script may use java.io.File objects as simple pointers to the local file system and EntityFS objects when more advanced file system operations are needed, as shown in Example 4.2, “Compile Java files and build a Jar file”.

The example below shows how different types can be used as the source property for a CopyTF.

Example 4.3. Using different source types with CopyTF

Groovy

import java.io.File import org.entityfs.util.ManualNamedReadableFile import org.schmant.task.io.CopyTF import org.schmant.support.entityfs.SchmantFileSystems // d is the target directory. It can, for instance, be a File or a Directory. // Copy a java.io.File: new CopyTF(). setSource(new File(props.getStringValue("java.io.tmpdir"), "f1")). setTarget(d).run() // Copy a (read only) EntityFS file entity (EFile): new CopyTF(). setSource( // Get a read only EFile entity for the file. SchmantFileSystems.getEntityForFile( new File(props.getStringValue("java.io.tmpdir"), "f2"), true)). setTarget(d).run() // Copy an inline file new CopyTF(). setSource( new ManualNamedReadableFile( "f3", "Contents of f3")). setTarget(d).run()

JavaScript

// d is the target directory. It can, for instance, be a File or a Directory. // Copy a java.io.File: new CopyTF(). setSource(new File(props.getStringValue("java.io.tmpdir"), "f1")). setTarget(d).run(); // Copy a (read only) EntityFS file entity (EFile): new CopyTF(). setSource( // Get a read only EFile entity for the file. SchmantFileSystems.getEntityForFile( new File(props.getStringValue("java.io.tmpdir"), "f2"), true)). setTarget(d).run(); // Copy an inline file new CopyTF(). setSource( new ManualNamedReadableFile( "f3", "Contents of f3")). setTarget(d).run();

JRuby

# d is the target directory. It can, for instance, be a File or a Directory. # Copy a java.io.File: Schmant::CopyTF.new. setSource( Java::JavaIo::File.new($props.getStringValue("java.io.tmpdir"), "f1")). setTarget($d).run # Copy a (read only) EntityFS file entity (EFile): Schmant::CopyTF.new. setSource( # Get a read only EFile entity for the file. Schmant::SchmantFileSystems.getEntityForFile( Java::JavaIo::File.new($props.getStringValue("java.io.tmpdir"), "f2"), true)). setTarget($d).run # Copy an inline file Schmant::CopyTF.new. setSource( Schmant::ManualNamedReadableFile.new( "f3", "Contents of f3")). setTarget($d).run

Jython

# d is the target directory. It can, for instance, be a File or a Directory. # Copy a java.io.File: CopyTF(). \ setSource(File(props.getStringValue("java.io.tmpdir"), "f1")). \ setTarget(d).run() # Copy a (read only) EntityFS file entity (EFile): # # Get a read only EFile entity for the file. CopyTF(). \ setSource( \ SchmantFileSystems.getEntityForFile( \ File(props.getStringValue("java.io.tmpdir"), "f2"), True)). \ setTarget(d).run(); # Copy an inline file CopyTF(). \ setSource( \ ManualNamedReadableFile( \ "f3", \ "Contents of f3")). \ setTarget(d).run()

The reference documentation for each task describes how it interprets each of its interpreted properties. Most tasks use the methods of ArgumentInterpreter, but some tasks have custom interpretation methods of their own. ArgumentInterpreter is described in more detail, along with descriptions of possible implementations of different interpreted types, in Appendix A, ArgumentInterpreter.

If a task factory property can take a collection of objects, for instance several source files for copying, it flattens the arguments given to the property's setter method. This means that the build script can give one or several arguments to the method. Multiple arguments can be stored in arrays or Collection:s. The arrays or collections may even contain other arrays or collections. All arguments are flattened to a single list using a FlatteningList.

Example 4.4. Flattening an argument list

Groovy

import CopyTF // Use a two-dimensional Groovy array as the source to a copy task. // Copy the files f1, f2, f3 and f4 to the directory d. new CopyTF(). addSources([[f1, f2], [f3], f4]). setTarget(d).run()

JavaScript

// Use a two-dimensional JavaScript array as the source to a copy task. // Copy the files f1, f2, f3 and f4 to the directory d. new CopyTF(). addSources([[f1, f2], [f3], f4]). setTarget(d).run();

JRuby

# Use a two-dimensional Ruby list as the source to a copy task. # Copy the files f1, f2, f3 and f4 to the directory d. Schmant::CopyTF.new. addSources([[$f1, $f2], [$f3], $f4]). setTarget($d).run

Jython

# Use a two-dimensional Python list as the source to a copy task. # Copy the files f1, f2, f3 and f4 to the directory d. CopyTF(). \ addSources([[f1, f2], [f3], f4]). \ setTarget(d).run()

There are two ways of representing the target of a generator task – using a File or using a FutureEntity such as a FutureFile or a FutureDirectory. Both are references to the location where the generator task will put its result.

After the generator task has been run, the created entity can be retrieved from the FutureEntity object. This makes it possible to use future entities for representing source entities that do not yet exist when using a TaskExecutor for running tasks, see the section called “Task executors and future entities” for an example.

For an interpreted task factory property, instead of setting the property value directly, a build script may instead set a Producer object that will yield the property value when the task is run. This is useful for setting properties that may not yet be created when the task is configured, and may be a little more compact than using a closure.

All generator tasks and some non-generator tasks are Producer:s. There are also a few other Producer implementations, such as the FutureProperty that produces the property value of another object.

The following example shows how an object produced by one task is fed to another task.

Example 4.5. Using a produced file as the source in another task.

Groovy

import java.text.SimpleDateFormat import java.util.Date import org.entityfs.util.CharSequenceReadableFile import org.schmant.support.FutureFile import org.schmant.support.entityfs.SchmantFileSystems import org.schmant.task.io.CopyTF import org.schmant.task.io.gzip.GZipTF // Create the file timestamp.gz in the directory dir. dir may be a File // directory or an EntityFS Directory // An in-memory temporary directory that will hold the intermediate file. def tmpDir = SchmantFileSystems.createRamFileSystem() new GZipTF(). // Set the task producing the timestamp file as the source. setSource( // The object produced by this task is the target file. new CopyTF(). setSource( new CharSequenceReadableFile( "timestamp: " + new SimpleDateFormat("yyyyMMdd HH:mm:ss").format(new Date()))). setTarget( new FutureFile(tmpDir, "timestamp")). // Running the task returns the Task object, which in this case is // also a Producer. run()). // The target for the gzip task. setTarget( new FutureFile(targetDir, "timestamp.gz")).run()

JavaScript

// Create the file timestamp.gz in the directory dir. dir may be a File // directory or an EntityFS Directory // An in-memory temporary directory that will hold the intermediate file. tmpDir = SchmantFileSystems.createRamFileSystem(); new GZipTF(). // Set the task producing the timestamp file as the source. setSource( // The object produced by this task is the target file. new CopyTF(). setSource( new CharSequenceReadableFile( "timestamp: " + new SimpleDateFormat("yyyyMMdd HH:mm:ss").format(new Date()))). setTarget( new FutureFile(tmpDir, "timestamp")). // Running the task returns the Task object, which in this case is // also a Producer. run()). // The target for the gzip task. setTarget( new FutureFile(targetDir, "timestamp.gz")).run();

JRuby

# Create the file timestamp.gz in the directory dir. dir may be a File # directory or an EntityFS Directory # An in-memory temporary directory that will hold the intermediate file. tmpDir = Schmant::SchmantFileSystems.createRamFileSystem Schmant::GZipTF.new. # Set the task producing the timestamp file as the source. setSource( # The object produced by this task is the target file. Schmant::CopyTF.new. setSource( Schmant::CharSequenceReadableFile.new( "timestamp: " + Java::JavaText::SimpleDateFormat.new("yyyyMMdd HH:mm:ss"). format(Java::JavaUtil::Date.new))). setTarget( Schmant::FutureFile.new(tmpDir, "timestamp")). # Running the task returns the Task object, which in this case is # also a Producer. run). # The target for the gzip task. setTarget( Schmant::FutureFile.new($targetDir, "timestamp.gz")).run

Jython

# Create the file timestamp.gz in the directory dir. dir may be a File # directory or an EntityFS Directory # An in-memory temporary directory that will hold the intermediate file. tmpDir = SchmantFileSystems.createRamFileSystem() # SimpleDateFormat is not automatically imported by the preparation # script. from java.text import SimpleDateFormat # Running the CopyTF task returns the Task object, which in this case is also # a Producer. This is then used as the source of the gzip task. GZipTF(). \ setSource( \ CopyTF(). \ setSource( \ CharSequenceReadableFile( \ "timestamp: " + \ SimpleDateFormat("yyyyMMdd HH:mm:ss"). \ format(Date()))). \ setTarget( \ FutureFile(tmpDir, "timestamp")). \ run()). \ setTarget( \ FutureFile(targetDir, "timestamp.gz")).run()


A build script may need to run a task for each file in a directory hierarchy, for instance when processing text files like in the examples below. This can be accomplished in a couple of ways. The most straightforward is perhaps to use an EntityFS FilteringIterator together with a DepthFirstIterator like in the example below.

Example 4.6. Set version information in all XML files in a directory hierarchy, overwriting the original files, using a recursive iterator.

Groovy

import org.entityfs.util.* import org.entityfs.util.filter.entity.EFileNameExtensionFilter import org.entityfs.util.itr.FilteringIterator import org.schmant.support.FutureFile import org.schmant.task.text.TextReplaceTF // Process all XML files from the directory hierarchy under the EntityFS // Directory src. Change all occurrences of the text !!!VERSION!!! // for the contents of the variable version. // NOTE: This is not the easiest way to accomplish this. See the next example! // Create a recursive, filtered iterator that returns all XML files in the // directory hierarchy def itr = new FilteringIterator( // It does not matter if a depth first or breadth first iterator is used here. Directories.getDepthFirstIterator(src), // The EFileNameExtensionFilter does only return EntityFS EFile:s. new EFileNameExtensionFilter("xml")) // Iterate over all files returned by the iterator while(itr.hasNext()) { def f = itr.next() def fname = Entities.getName(f) // The temporary file to put the process result in. def tmpTarget = new FutureFile( // Get f's parent directory. Entities.getParent(f), fname + ".tmp") // The text processing task new TextReplaceTF(). addReplace("!!!VERSION!!!", version). setSource(f). setTarget(tmpTarget).run() // Replace the source file with the processed file. // // Instead of replacing the source file manually, the TextReplaceTF could have // been wrapped in a ReplaceSourceFileTF. See the next example. Entities.delete(f) Entities.rename(tmpTarget.getEntity(), fname) }

JavaScript

// Process all XML files from the directory hierarchy under the EntityFS // Directory src. Change all occurrences of the text !!!VERSION!!! // for the contents of the variable version. // NOTE: This is not the easiest way to accomplish this. See the next example! // Create a recursive, filtered iterator that returns all XML files in the // directory hierarchy itr = new FilteringIterator( // It does not matter if a depth first or breadth first iterator is used here. Directories.getDepthFirstIterator(src), // The EFileNameExtensionFilter does only return EntityFS EFile:s. new EFileNameExtensionFilter("xml")); // Iterate over all files returned by the iterator while(itr.hasNext()) { f = itr.next(); fname = Entities.getName(f); // The temporary file to put the process result in. tmpTarget = new FutureFile( // Get f's parent directory. Entities.getParent(f), fname + ".tmp"); // The text processing task new TextReplaceTF(). addReplace("!!!VERSION!!!", version). setSource(f). setTarget(tmpTarget).run(); // Replace the source file with the processed file. // We have to use Entities.deleteEntity here instead of Entities.delete since // "delete" is a reserved word in JavaScript. // // Instead of replacing the source file manually, the TextReplaceTF could have // been wrapped in a ReplaceSourceFileTF. See the next example. Entities.deleteEntity(f); Entities.rename(tmpTarget.getEntity(), fname); }

JRuby

# Process all XML files from the directory hierarchy under the EntityFS # Directory src. Change all occurrences of the text !!!VERSION!!! # for the contents of the variable version. # NOTE: This is not the easiest way to accomplish this. See the next example! # Create a recursive, filtered iterator that returns all XML files in the # directory hierarchy itr = Schmant::FilteringIterator.new( # It does not matter if a depth first or breadth first iterator is used here. Schmant::Directories.getDepthFirstIterator($src), # The EFileNameExtensionFilter does only return EntityFS EFile:s. Schmant::EFileNameExtensionFilter.new("xml")) # Iterate over all files returned by the iterator while itr.hasNext() f = itr.next() fname = Schmant::Entities.getName(f) # The temporary file to put the process result in. tmpTarget = Schmant::FutureFile.new( # Get f's parent directory. Schmant::Entities.getParent(f), fname + ".tmp") # The text processing task Schmant::TextReplaceTF.new. addReplace("!!!VERSION!!!", $version). setSource(f). setTarget(tmpTarget).run() # Replace the source file with the processed file. # # Instead of replacing the source file manually, the TextReplaceTF could have # been wrapped in a ReplaceSourceFileTF. See the next example. Schmant::Entities.delete(f) Schmant::Entities.rename(tmpTarget.getEntity(), fname) end

Jython

# Process all XML files from the directory hierarchy under the EntityFS # Directory src. Change all occurrences of the text !!!VERSION!!! # for the contents of the variable version. # NOTE: This is not the easiest way to accomplish this. See the next example! # Create a recursive, filtered iterator that returns all XML files in the # directory hierarchy # # It does not matter if a depth first or breadth first iterator is used here. # # The EFileNameExtensionFilter does only return EntityFS EFile:s. itr = FilteringIterator( \ Directories.getDepthFirstIterator(src), \ EFileNameExtensionFilter("xml")) # Iterate over all files returned by the iterator while itr.hasNext(): f = itr.next() fname = Entities.getName(f) # The temporary file to put the process result in. tmpTarget = FutureFile( \ Entities.getParent(f), \ fname + ".tmp") # The text processing task TextReplaceTF(). \ addReplace("!!!VERSION!!!", version). \ setSource(f). \ setTarget(tmpTarget).run() # Replace the source file with the processed file. # # Instead of replacing the source file manually, the TextReplaceTF could have # been wrapped in a ReplaceSourceFileTF. See the next example. Entities.delete(f) Entities.rename(tmpTarget.getEntity(), fname)

A simpler and more compact solution, however, is to use a RecursiveActionTF or a RecursiveProcessTF.

Example 4.7. Set version information in all XML files in a directory hierarchy, overwriting the original files, using a RecursiveActionTF

Groovy

import org.entityfs.util.filter.entity.EFileNameExtensionFilter import org.schmant.arg.DirectoryAndFilter import org.schmant.task.meta.RecursiveActionTF import org.schmant.task.proxy.ReplaceSourceFileTF import org.schmant.task.text.TextReplaceTF // Process all XML files from the directory hierarchy under the directory src. // Change all occurrences of the text !!!VERSION!!! for the contents of the // variable version. // // Use a RecursiveActionTF since we use a ReplaceSourceFileTF to replace the // source file. ReplaceSourceFileTF is an action task since it does not have a // separate target property. new RecursiveActionTF(). // src may for instance be a java.io.File directory or an EntityFS // Directory setSource(new DirectoryAndFilter(src, new EFileNameExtensionFilter("xml"))). setTaskFactory( // The ReplaceSourceFileTF sets the target property of its nested task to be // a temporary file. When the nested task has run, it overwrites the source // file with the contents of the temporary file. new ReplaceSourceFileTF(). setTaskFactory( new TextReplaceTF(). addReplace("!!!VERSION!!!", version))).run()

JavaScript

// Process all XML files from the directory hierarchy under the directory src. // Change all occurrences of the text !!!VERSION!!! for the contents of the // variable version. // // Use a RecursiveActionTF since we use a ReplaceSourceFileTF to replace the // source file. ReplaceSourceFileTF is an action task since it does not have a // separate target property. new RecursiveActionTF(). // src may for instance be a java.io.File directory or an EntityFS // Directory setSource(new DirectoryAndFilter(src, new EFileNameExtensionFilter("xml"))). setTaskFactory( // The ReplaceSourceFileTF sets the target property of its nested task to be // a temporary file. When the nested task has run, it overwrites the source // file with the contents of the temporary file. new ReplaceSourceFileTF(). setTaskFactory( new TextReplaceTF(). addReplace("!!!VERSION!!!", version))).run();

JRuby

# Process all XML files from the directory hierarchy under the directory src. # Change all occurrences of the text !!!VERSION!!! for the contents of the # variable version. # # Use a RecursiveActionTF since we use a ReplaceSourceFileTF to replace the # source file. ReplaceSourceFileTF is an action task since it does not have a # separate target property. Schmant::RecursiveActionTF.new. # src may for instance be a java.io.File directory or an EntityFS # Directory setSource(Schmant::DirectoryAndFilter.new( $src, Schmant::EFileNameExtensionFilter.new("xml"))). setTaskFactory( # The ReplaceSourceFileTF sets the target property of its nested task to be # a temporary file. When the nested task has run, it overwrites the source # file with the contents of the temporary file. Schmant::ReplaceSourceFileTF.new. setTaskFactory( Schmant::TextReplaceTF.new. addReplace("!!!VERSION!!!", $version))).run

Jython

# Process all XML files from the directory hierarchy under the directory src. # Change all occurrences of the text !!!VERSION!!! for the contents of the # variable version. # # Use a RecursiveActionTF since we use a ReplaceSourceFileTF to replace the # source file. ReplaceSourceFileTF is an action task since it does not have a # separate target property. # # src may for instance be a java.io.File directory or an EntityFS Directory # # The ReplaceSourceFileTF sets the target property of its nested task to be a # temporary file. When the nested task has run, it overwrites the source file # the contents of the temporary file. RecursiveActionTF(). \ setSource(DirectoryAndFilter(src, EFileNameExtensionFilter("xml"))). \ setTaskFactory( \ ReplaceSourceFileTF(). \ setTaskFactory( \ TextReplaceTF(). \ addReplace("!!!VERSION!!!", version))).run()

The two examples above does exactly the same thing. RecursiveActionTF used together with ReplaceSourceFileTF solves the problem of keeping the results from the text processing task in a temporary file before the original file is replaced, so that that does not have to be implemented in the build script.

If the build script does not want to overwrite the original files, but instead wants to put the results from the text processing in a new location, it can use the RecursiveProcessTF. It creates the target directory hierarchy and puts the created files where they should be.

Example 4.8. Set version information in all XML files in a directory hierarchy, putting the target files in a new directory hierarchy, using a recursive iterator.

Groovy

import org.entityfs.util.* import org.entityfs.util.filter.entity.EFileNameExtensionFilter import org.entityfs.util.itr.FilteringIterator import org.schmant.support.FutureFile import org.schmant.task.text.TextReplaceTF // Copy all XML files from the directory hierarchy under the directory src to // a directory hierarchy under the directory target. Change all occurrences of // the text !!!VERSION!!! for the contents of the variable version. // // Create a recursive, filtered iterator that returns all XML files in the // directory hierarchy def itr = new FilteringIterator( // It does not matter if a depth first or depth last iterator is used here Directories.getDepthFirstIterator(src), // The EFileNameExtensionFilter does only return _files_. new EFileNameExtensionFilter("xml")) // Iterate over all files returned by the iterator while(itr.hasNext()) { def f = itr.next() // f's location relative to src def floc = Entities.getRelativeLocation(f, src) // The location of the target file def tloc = new FutureFile(target, floc) // The text processing task new TextReplaceTF(). addReplace("!!!VERSION!!!", version). setSource(f). setTarget(tloc).run() }

JavaScript

// Copy all XML files from the directory hierarchy under the directory src to // a directory hierarchy under the directory target. Change all occurrences of // the text !!!VERSION!!! for the contents of the variable version. // // Create a recursive, filtered iterator that returns all XML files in the // directory hierarchy itr = new FilteringIterator( // It does not matter if a depth first or depth last iterator is used here Directories.getDepthFirstIterator(src), // The EFileNameExtensionFilter does only return _files_. new EFileNameExtensionFilter("xml")); // Iterate over all files returned by the iterator while(itr.hasNext()) { f = itr.next(); // f's location relative to src floc = Entities.getRelativeLocation(f, src); // The location of the target file tloc = new FutureFile(target, floc); // The text processing task new TextReplaceTF(). addReplace("!!!VERSION!!!", version). setSource(f). setTarget(tloc).run(); }

JRuby

# Copy all XML files from the directory hierarchy under the directory src to # a directory hierarchy under the directory target. Change all occurrences of # the text !!!VERSION!!! for the contents of the variable version. # # Create a recursive, filtered iterator that returns all XML files in the # directory hierarchy itr = Schmant::FilteringIterator.new( # It does not matter if a depth first or depth last iterator is used here Schmant::Directories.getDepthFirstIterator($src), # The EFileNameExtensionFilter does only return _files_. Schmant::EFileNameExtensionFilter.new("xml")) # Iterate over all files returned by the iterator while itr.hasNext f = itr.next # f's location relative to src floc = Schmant::Entities.getRelativeLocation(f, $src) # The location of the target file tloc = Schmant::FutureFile.new($target, floc) # The text processing task Schmant::TextReplaceTF.new. addReplace("!!!VERSION!!!", $version). setSource(f). setTarget(tloc).run end

Jython

# Copy all XML files from the directory hierarchy under the directory src to # a directory hierarchy under the directory target. Change all occurrences of # the text !!!VERSION!!! for the contents of the variable version. # # Create a recursive, filtered iterator that returns all XML files in the # directory hierarchy # # It does not matter if a depth first or depth last iterator is used here # # The EFileNameExtensionFilter does only return _files_. itr = FilteringIterator( \ Directories.getDepthFirstIterator(src), \ EFileNameExtensionFilter("xml")) # Iterate over all files returned by the iterator while itr.hasNext(): f = itr.next() # f's location relative to src floc = Entities.getRelativeLocation(f, src) # The location of the target file tloc = FutureFile(target, floc) # The text processing task TextReplaceTF(). \ addReplace("!!!VERSION!!!", version). \ setSource(f). \ setTarget(tloc).run()

Example 4.9. Set version information in all XML files in a directory hierarchy, putting the target files in a new directory hierarchy.

Groovy

import org.entityfs.util.filter.entity.EFileNameExtensionFilter import org.schmant.arg.DirectoryAndFilter import org.schmant.task.meta.RecursiveProcessTF import org.schmant.task.text.TextReplaceTF // Copy all XML files from the directory hierarchy under the directory src to // a directory hierarchy under the directory target. Change all occurrences of // the text !!!VERSION!!! for the contents of the variable version. // // Use RecursiveProcessTF since the nested task factory needs both a source and // a target property. new RecursiveProcessTF(). // src may for instance be a java.io.File directory or an EntityFS // Directory setSource(new DirectoryAndFilter(src, new EFileNameExtensionFilter("xml"))). setTarget(target). setTaskFactory( new TextReplaceTF(). addReplace("!!!VERSION!!!", version)).run()

JavaScript

// Copy all XML files from the directory hierarchy under the directory src to // a directory hierarchy under the directory target. Change all occurrences of // the text !!!VERSION!!! for the contents of the variable version. // // Use RecursiveProcessTF since the nested task factory needs both a source and // a target property. new RecursiveProcessTF(). // src may for instance be a java.io.File directory or an EntityFS // Directory setSource(new DirectoryAndFilter(src, new EFileNameExtensionFilter("xml"))). setTarget(target). setTaskFactory( new TextReplaceTF(). addReplace("!!!VERSION!!!", version)).run();

JRuby

# Copy all XML files from the directory hierarchy under the directory src to a # directory hierarchy under the directory target. Change all occurrences of the # text !!!VERSION!!! for the contents of the variable version. # # Use RecursiveProcessTF since the nested task factory needs both a source and a # target property. Schmant::RecursiveProcessTF.new. # src may for instance be a java.io.File directory or an EntityFS Directory setSource(Schmant::DirectoryAndFilter.new($src, Schmant::EFileNameExtensionFilter.new("xml"))). setTarget($target). setTaskFactory( Schmant::TextReplaceTF.new. addReplace("!!!VERSION!!!", $version)).run

Jython

# Copy all XML files from the directory hierarchy under the directory src to a # directory hierarchy under the directory target. Change all occurrences of the # text !!!VERSION!!! for the contents of the variable version. # # Use RecursiveProcessTF since the nested task factory needs both a source and a # target property. # # src may for instance be a java.io.File directory or an EntityFS # Directory RecursiveProcessTF(). \ setSource(DirectoryAndFilter( \ src, \ EFileNameExtensionFilter("xml"))). \ setTarget(target). \ setTaskFactory( \ TextReplaceTF(). \ addReplace("!!!VERSION!!!", version)).run()

To determine where to put the target entities, the RecursiveProcessTF uses can use either a TargetStrategy or a closure object. In the examples above, the default target strategy DefaultTargetStrategy is used. It puts the created entities in the same location relative to the target directory as the source entities have relative to the source directory.

The table below lists the different available TargetStrategy:s.


If a closure is used, it should take three parameters – an EntityView (the processed entity), a DirectoryView (the target base directory) and a RelativeLocation (the source entity's location relative to the source base directory). When executed, the closure should return something that can be interpreted by the future entity interpreter (a FutureEntity, for instance).

The following example shows how a closure can be used to put big XML files in one directory hierarchy and small XML files in another.

Example 4.10. Set version information in all XML files in a directory hierarchy, putting the target files in a two new directory hierarchies.

Groovy

import org.entityfs.el.RelativeLocation import org.entityfs.util.Files import org.entityfs.util.filter.entity.EFileNameExtensionFilter import org.schmant.arg.DirectoryAndFilter import org.schmant.support.FutureFile import org.schmant.task.meta.RecursiveProcessTF import org.schmant.task.text.TextReplaceTF // src is an EntityFS DirectoryView. new RecursiveProcessTF(). setSource(new DirectoryAndFilter(src, new EFileNameExtensionFilter("xml"))). setTarget(target). setTargetStrategy({ // A closure source, targetDir, relLoc -> // Get the file size. The filter ensures that this closure will only be // called with file sources. def size = Files.getSize(source) def base if (size < 2048) { // Put it in the "small" hierarchy base = "small/" } else { // Put it in the "big" hierarchy base = "big/" } // The relLoc argument is the source file's location relative to the source // base directory (src) def loc = new RelativeLocation(base + relLoc.location) return new FutureFile(targetDir, loc) }). setTaskFactory( new TextReplaceTF(). addReplace("!!!VERSION!!!", version)).run()

JavaScript

// src is an EntityFS DirectoryView. new RecursiveProcessTF(). setSource(new DirectoryAndFilter(src, new EFileNameExtensionFilter("xml"))). setTarget(target). setTargetStrategy( // A closure function(source, targetDir, relLoc) { // Get the file size. The filter ensures that this closure will only be // called with file sources. var size = Files.getSize(source); if (size < 2048) { // Put it in the "small" hierarchy var base = "small/"; } else { // Put it in the "big" hierarchy var base = "big/"; } // The relLoc argument is the source file's location relative to the // source base directory (src) var loc = new RelativeLocation(base + relLoc.getLocation()); return new FutureFile(targetDir, loc); }). setTaskFactory( new TextReplaceTF(). addReplace("!!!VERSION!!!", version)).run();

JRuby

# src is an EntityFS DirectoryView. # # Use Proc.new to create a new function object Schmant::RecursiveProcessTF.new. setSource( Schmant::DirectoryAndFilter.new( $src, Schmant::EFileNameExtensionFilter.new("xml"))). setTarget($target). setTargetStrategy( Proc.new { |source, targetDir, relLoc| # Get the size of the source file. The filter ensures that this closure # will only be called with file arguments. size = Schmant::Files.getSize(source) if size < 2048 then # Put the file in the "small" hierarchy base = "small/" else # Put the file in the "big" hierarchy base = "big/" end # The "relLoc" variable is set to the source entity's location relative to # the source base directory (src). loc = Schmant::RelativeLocation.new(base + relLoc.location) # This object is returned from the script. Schmant::FutureFile.new(targetDir, loc) }). setTaskFactory( Schmant::TextReplaceTF.new. addReplace("!!!VERSION!!!", $version)).run

Jython

# src is an EntityFS DirectoryView. # # Jython lacks closure support. We can get pretty close by using lambda # expression or function variables. # Define the function that will be used to calculate the target location. Since # this function spans several lines, we cannot use an inline lambda expression def generateTargetLocation(source, targetDir, relLoc): # Get the file size. The filter ensures that this closure will only be called # with file sources. size = Files.getSize(source) if size < 2048: # Put it in the "small" hierarchy base = "small/" else: # Put it in the "big" hierarchy base = "big/" # The relLoc argument is the source file's location relative to the source # base directory (src) loc = RelativeLocation(base + relLoc.getLocation()) return FutureFile(targetDir, loc) RecursiveProcessTF(). \ setSource(DirectoryAndFilter(src, EFileNameExtensionFilter("xml"))). \ setTarget(target). \ setTargetStrategy(generateTargetLocation). \ setTaskFactory( TextReplaceTF(). addReplace("!!!VERSION!!!", version)).run()

In addition to all tasks, Schmant script programmers have access to an environment that contains support and utility classes as well as some built-in script functions. The built-in functions are script language-dependent and are discussed in the scripting language guides.

Information and error output from scripts and tasks are written to a Report instance. Every execution thread has its own instance which can be retrieved using the ReportManager method getReport(). For some script languages, the environment preparation script defines convenience functions for logging from a script, see the scripting language guides. Scripts written in other languages may use the Report methods directly, like ReportManager.getReport().info("Info, info").

Just like any other logging API, output to a Report is categorized using a log level. Report uses five different levels.


The "Flags" column in the table above contains the flags that is used for starting Schmant with the different log levels.

By default, the lowest level logged is info, which means that debug and trace messages are discarded.

The log level can be changed in several ways:

When a new execution thread is created, for instance by a TaskExecutor, the created thread inherits the log level from its parent thread.

There are different Report implementations. By default StdoutReport is used. It logs output to standard out and standard err. Use the -r argument when launching Schmant to use another ReportFactory for creating other types of Report:s.

The RedirectReportTF task can be used to redirect the output from a nested task to a file.

Schmant makes heavy use of EntityFS and build scripts do also have access to it. EntityFilter:s, EntityFS' utility classes and iterators are expressive tools for working with files and directories. See below for an example.

Example 5.1. Iterating over files and directories

Groovy

import java.util.concurrent.TimeUnit import org.entityfs.util.* import org.entityfs.util.filter.entity.* import org.entityfs.util.filter.regexp.EntityNameGlobFilter import org.entityfs.util.itr.FilteringIterator // Write, to stdout, the contents of all XML files that have not been modified // in the last six hours and that have a directory with a name that starts with // "here" somewhere in their search paths. Iterate in the directory hierarchy // under the Directory d. // // Filters can be combined using Groovy operators (& is and) itr = new FilteringIterator( Directories.getDepthLastIterator(d), new EFileNameExtensionFilter("xml") & new EntityRecentModificationFilter(6, TimeUnit.HOURS) & new SuperParentOrFilter( new EntityNameGlobFilter("here*"))) while(itr.hasNext()) { java.lang.System.out.println(Files.readTextFile(itr.next())) }

JavaScript

// Write, to stdout, the contents of all XML files that have not been modified // in the last six hours and that have a directory with a name that starts with // "here" somewhere in their search paths. Iterate in the directory hierarchy // under the Directory d. itr = new FilteringIterator( Directories.getDepthLastIterator(d), new EFileNameExtensionFilter("xml").and( new EntityRecentModificationFilter(6, TimeUnit.HOURS).and( new SuperParentOrFilter( new EntityNameGlobFilter("here*"))))); while(itr.hasNext()) { Packages.java.lang.System.out.println(Files.readTextFile(itr.next())); }

JRuby

# Write, to stdout, the contents of all XML files that have not been modified # in the last six hours and that have a directory with a name that starts with # "here" somewhere in their search paths. Iterate in the directory hierarchy # under the Directory d. itr = Schmant::FilteringIterator.new( Schmant::Directories.getDepthLastIterator($d), Schmant::EFileNameExtensionFilter.new("xml").and( Schmant::EntityRecentModificationFilter.new( 6, Java::JavaUtilConcurrent::TimeUnit::HOURS).and( Schmant::SuperParentOrFilter.new( Schmant::EntityNameGlobFilter.new("here*"))))) while itr.hasNext do Java::JavaLang::java.lang.System.out.println(Schmant::Files.readTextFile(itr.next)) end

Jython

# Write, to stdout, the contents of all XML files that have not been modified # in the last six hours and that have a directory with a name that starts with # "here" somewhere in their search paths. Iterate in the directory hierarchy # under the Directory d. # # java.util.concurrent classes are not imported automatically from java.util.concurrent import TimeUnit itr = FilteringIterator( \ Directories.getDepthLastIterator(d), \ EFileNameExtensionFilter("xml").and( \ EntityRecentModificationFilter(6, TimeUnit.HOURS).and( \ SuperParentOrFilter( \ EntityNameGlobFilter("here*"))))); while(itr.hasNext()): java.lang.System.out.println(Files.readTextFile(itr.next()));

A common trick when using files checked out from Subversion is to hide all .svn directories using an EntityFilter.

Example 5.2. Hiding .svn directories when creating an EclipseWorkspace

Groovy

import org.entityfs.util.filter.entity.EntityNameFilter import org.schmant.project.eclipse.EclipseWorkspace // Create an EclipseWorkspace from the contents of directory wos // Hide all .svn directories. // // ~ is used to negate the filter def wWos = new EclipseWorkspace( wos.newView(~(new EntityNameFilter(".svn")))) // Now the .svn directories in the workspace will be invisible.

JavaScript

// Create an EclipseWorkspace from the contents of directory wos // Hide all .svn directories. wWos = new EclipseWorkspace( wos.newView(new EntityNameFilter(".svn").not())); // Now the .svn directories in the workspace will be invisible.

JRuby

# Create an EclipseWorkspace from the contents of directory wos # Hide all .svn directories. wWos = Schmant::EclipseWorkspace.new( $wos.newView(Schmant::EntityNameFilter.new(".svn").not)) # Now the .svn directories in the workspace will be invisible.

Jython

# Create an EclipseWorkspace from the contents of directory wos # Hide all .svn directories. wWos = EclipseWorkspace( \ wos.newView(EntityNameFilter(".svn").not())) # Now the .svn directories in the workspace will be invisible.

Build scripts are in no way required to use EntityFS objects. One reason for not doing that is that Java's File:s are easier to use for someone that is not familiar with EntityFS. A script may also mix and match EntityFS entities and Java File:s. The next example shows how to move between the two worlds.

Example 5.3. From Java files to EntityFS and back again

Groovy

import java.io.File import org.entityfs.util.cap.entity.ECFileResolvableUtil import org.entityfs.util.cap.fs.FSCFileResolvableUtil import org.schmant.support.entityfs.SchmantFileSystems // Create a new File object for the directory /home/me/workspace: def d = new File("/home/me/workspace") // Create a new read/write and locking EntityFS FileSystem with the root // directory in d. The method returns the file system's root directory. def rootDir = SchmantFileSystems.getEntityForDirectory(d, false) // Get the File object representing the root directory. def rootDirFile = ECFileResolvableUtil.getFileObject(rootDir) // Now rootDirFile.getCanonicalPath().equals(d.getCanonicalPath()) // (the getCanonicalPath() will resolve all symbolic links in the paths) // Get the entity in the file system fs that corresponds to the File rootDirFile def rd = FSCFileResolvableUtil.getEntityForFile( rootDir.getFileSystem(), rootDirFile) // Now rd == rootDir

JavaScript

// Create a new File object for the directory /home/me/workspace: d = new File("/home/me/workspace"); // Create a new read/write and locking EntityFS FileSystem with the root // directory in d. The method returns the file system's root directory. rootDir = SchmantFileSystems.getEntityForDirectory(d, false); // Get the File object representing the root directory. rootDirFile = ECFileResolvableUtil.getFileObject(rootDir); // Now rootDirFile.getCanonicalPath().equals(d.getCanonicalPath()) // (the getCanonicalPath() will resolve all symbolic links in the paths) // Get the entity in the file system fs that corresponds to the File rootDirFile rd = FSCFileResolvableUtil.getEntityForFile( rootDir.getFileSystem(), rootDirFile); // Now rd == rootDir

JRuby

# Create a new File object for the directory /home/me/workspace: d = Java::JavaIo::File.new("/home/me/workspace") # Create a new read/write and locking EntityFS FileSystem with the root # directory in d. The method returns the file system's root directory. rootDir = Schmant::SchmantFileSystems.getEntityForDirectory(d, false) # Get the File object representing the root directory. rootDirFile = Schmant::ECFileResolvableUtil.getFileObject(rootDir) # Now rootDirFile.getCanonicalPath().equals(d.getCanonicalPath()) # (the getCanonicalPath() will resolve all symbolic links in the paths) # Get the entity in the file system fs that corresponds to the File rootDirFile # The org.entityfs.util.cap.fs package is not included in the Schmant module. rd = Java::OrgEntityfsUtilCapFs::FSCFileResolvableUtil.getEntityForFile( rootDir.getFileSystem(), rootDirFile) # Now rd == rootDir

Jython

# Create a new File object for the directory /home/me/workspace: d = File("/home/me/workspace") # Create a new read/write and locking EntityFS FileSystem with the root # directory in d. The method returns the file system's root directory. rootDir = SchmantFileSystems.getEntityForDirectory(d, False) # Get the File object representing the root directory. rootDirFile = ECFileResolvableUtil.getFileObject(rootDir) # Now rootDirFile.getCanonicalPath().equals(d.getCanonicalPath()) # (the getCanonicalPath() will resolve all symbolic links in the paths) # FSCFileResolvableUtil is not imported by Schmant from org.entityfs.util.cap.fs import FSCFileResolvableUtil # Get the entity in the file system fs that corresponds to the File rootDirFile rd = FSCFileResolvableUtil.getEntityForFile( \ rootDir.getFileSystem(), \ rootDirFile) # Now rd == rootDir

All tasks are not EntityFS-aware, though. Tasks that use external libraries that don't use EntityFS or that launch external processes do not support EntityFS entities. For those tasks, EntityFS entities are translated to Java File:s in the argument interpretation process. Tasks that don't support EntityFS entities cannot use nice EntityFS features such as directory views or in-memory file systems. The task reference documentation for each Schmant task has information on whether it supports EntityFS entities or not.

EntityFS-aware tasks support locking file systems that prevent that parallel build threads accidentally modify the same files or directories. The file systems returned by SchmantFileSystems' methods are all locking, if that is not explicitly disabled.

For more information on using EntityFS with Schmant, see Appendix B, EntityFS cookbook.

The following variables are available to all running scripts:

propsProperties

Java system properties and properties set using the -p argument when launching Schmant.

argsList<String>

Script arguments given after the script file on the command line.

org_schmant_scriptFileEFile

The currently running script file (read only).

Environment variables are accessed through the java.lang.System.getenv method.

Schmant scripts can use EntityFS Properties like in the example below.

Example 5.4. Using EntityFS property methods

Groovy

import org.entityfs.util.Directories import org.entityfs.util.properties.PropertiesUtil // Load the build.properties file from the directory d def buildProperties = PropertiesUtil.loadFromFile( Directories.getFile(d, "build.properties")) // Set a couple of variables def version = buildProperties.getStringValue("version") def buildNo = buildProperties.getIntValue("buildNo", 17)

JavaScript

// Load the build.properties file from the directory d buildProperties = PropertiesUtil.loadFromFile( Directories.getFile(d, "build.properties")); // Set a couple of variables version = buildProperties.getStringValue("version"); buildNo = buildProperties.getIntValue("buildNo", 17);

JRuby

# Load the build.properties file from the directory d buildProperties = Schmant::PropertiesUtil.loadFromFile( Schmant::Directories.getFile($d, "build.properties")) # Set a couple of variables version = buildProperties.getStringValue("version") buildNo = buildProperties.getIntValue("buildNo", 17)

Jython

# Load the build.properties file from the directory d buildProperties = PropertiesUtil.loadFromFile( \ Directories.getFile(d, "build.properties")) # Set a couple of variables version = buildProperties.getStringValue("version") buildNo = buildProperties.getIntValue("buildNo", 17)

Schmant has several utility classes with static methods which can be used by build scripts.

ArgumentInterpreter

A singleton instance of the ArgumentInterpreter class is used by many tasks to interpret untyped arguments. See the section called “Interpreted arguments”. ArgumentInterpreter is seldom used directly by Schmant scripts.

DirectoryAndFilter

This is not a utility class per se, but it has the static listWithFilter method for creating a list of DirectoryAndFilter objects from a collection of directories and an EntityFilter. See for instance the task reference documentation for RecursiveProcessTF for examples.

JdkUtil

Utility methods for working with JRE and JDK installations.

Example 5.5. Using JdkUtil

Groovy

import org.schmant.support.* // Only do this if we're on Java 7 or newer if (JdkUtil.getCurrentJdkVersion().isSameOrNewerThan(new JdkVersion("1.7"))) { // ... do stuff } else { warn("This requires Java 7 or newer") }

JavaScript

// Only do this if we're on Java 7 or newer if (JdkUtil.getCurrentJdkVersion().isSameOrNewerThan(new JdkVersion("1.7"))) { // ... do stuff } else { warn("This requires Java 7 or newer"); }

JRuby

# Only do this if we're on Java 7 or newer if Schmant::JdkUtil.getCurrentJdkVersion().isSameOrNewerThan(Schmant::JdkVersion.new("1.7")) # ... do stuff else warn "This requires Java 7 or newer" end

Jython

# Only do this if we're on Java 7 or newer if JdkUtil.getCurrentJdkVersion().isSameOrNewerThan(JdkVersion("1.7")): debug("... do stuff") else: warn("This requires Java 7 or newer")

ProjectFilterUtil

Utilities for using project filters. See Chapter 8, Projects.

ReportManager

Holds a reference to each thread's Report object.

SchmantFileSystems

Utility methods for creating EntityFS FileSystem:s that a Schmant script may use. The created file systems have the SchmantReportLogAdapter log adapter set.

The SchmantFileSystems class also has the makeFileBacked and makeRandomlyAccessible for making EFile entities File-backed and FCRandomAccess:ible, respectively.

SchmantUtil

Various utility methods.

TempFileUtil

Utility methods for creating temporary files and directories. See the section called “Temporary files and directories”.

XmlCatalogResolver

XML catalog that can be used for resolving entities when parsing or XSL transforming XML files. See the XsltTF task factory documentation for examples.

In addition to Schmant's own utility classes, many of EntityFS' utility classes may be useful. See Appendix B, EntityFS cookbook.

Build scripts often have to use temporary files for storing data that is being processed, or sometimes even entire temporary directory hierarchies for, for example, compiled classes before they are added to a Jar file.

The TempFileUtil class has a set of static methods for creating temporary files and directories. Entities created using TempFileUtil are automatically deleted when Schmant exits (unless it was started with the -k flag).

As long as the tasks that are used are EntityFS-aware, temporary directories may well be in a RAM file system. The SchmantFileSystems.createRamFileSystem() method can be used to create a RAM file system configured for using with a build script. See the section called “In-memory file systems”.

By default, (process) tasks that use temporary directories use the default temporary directory of their target's file system. TemporaryDirectoryConfigurable task factories can be configured with a custom temporary files directory – a RAM file system directory, for instance.

Several related tasks are bundled into task packages. Task packages are used for distributing logical groupings of tasks and also for isolating tasks with conflicting library dependencies. A task package is a Zip file that contains task implementations and task reference documentation.

Schmant comes with a basic set of task packages. More task packages can be downloaded from the Schmant site or from elsewhere on the Internet (well, at least in the future, hopefully).

Extra task packages that scripts want to use are put in the Schmant installation's task directory or listed on the Schmant command line with the -t option. See Chapter 3, Running Schmant. The task package Zip file can be used directly, or it can be unpacked to a directory.

The org.schmant.task.base task package is enabled by default. Other task packages must be enabled by a build script before it can use it. Enabling a task package adds a collection of Jar files to the running script's classpath and imports a set of Java packages so that fully qualified class names don't have to be used to reference task factories and other classes in the task package.

For most script languages, a task package is enabled by calling the enableTaskPackage method. See the scripting language guides.

In the example below, the classes compiled in Example 4.2, “Compile Java files and build a Jar file” are analyzed with Findbugs. The ExtFindbugsTF task factory is in the net.findbugs task package that is distributed with Schmant.

Example 6.1. Enabling the Findbugs task package and running Findbugs.

Groovy

// enableTaskPackage net.findbugs // The net.findbugs task package is enabled in the script manifest above. The // task package has to be enabled before the script is loaded so that the // classes in the task package are available to be imported below. import java.io.File import java.text.SimpleDateFormat import java.util.Date import org.entityfs.util.Directories import org.schmant.support.io.TempFileUtil import org.schmant.support.entityfs.SchmantFileSystems import org.schmant.task.findbugs.* import org.schmant.task.jdk.jar.JarTF import org.schmant.task.jdk.javac.jdk6.Jdk6JavacTF // Compile files from the directory /home/me/myproject/src and put the resulting // class files in a temporary directory. The source files have compilation // dependencies to all Jar files in /home/me/myproject/lib and to the Jar file // /home/me/myproject/optlib/opt.jar // A temporary java.io.File directory for the compiled classes. This, along with // all of its contents, will be automatically deleted when Schmant exits (unless // the -k flag is used). def ctarget = TempFileUtil.createTempDir() // This is the lib directory expressed as a (read only) EntityFS Directory. // By using a Directory, the script can use the utility methods of // Directories to extract the files it wants. See below. def libDir = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/lib"), true) // Get all jar files from the lib directory. The jar files are returned as a // Set of EntityFS EFile:s def depjars = Directories.getAllFilesMatching(libDir, "*.jar") // Add a dependency from the optlib directory. This is a Java File depjars.add(new File("/home/me/myproject/optlib/opt.jar")) // Compile the Java source files. new Jdk6JavacTF(). addSource(new File("/home/me/myproject/src")). addClasspathEntries(depjars). setTarget(ctarget).run() // A timestamp for the built archive def timestamp = new SimpleDateFormat("yyyyMMddHHmm"). format(new Date()) // Build the Jar file. Put it in /home/me/myproject new JarTF(). addSource(ctarget). setTarget(new File("/home/me/myproject/myproject" + timestamp + ".jar")). run() // Run Findbugs. new ExtFindbugsTF(). // If the findbugsExecutable property is not set, the task looks for the // findbugs executable by checking if the FINDBUGS_HOME environment variable // is set. If not, it searches for findbugs in the directories referenced by // the PATH environment variable. // setFindbugsExecutable("c:\\Java\\findbugs\\bin\\findbugs.bat"). // // Add classpath dependencies that should not be analyzed addAuxClasspathEntries(depjars). // Add all source files addSourceCodeContainer(new File("/home/me/myproject/src")). // The compiled classes are located under the directory ctarget (and // subdirectories) addSource(ctarget). setFindbugsReportFormat(FindbugsReportFormat.HTML). setTarget( new File("/home/me/myproject/myproject" + timestamp + ".html")).run()

JavaScript

// Compile files from the directory /home/me/myproject/src and put the resulting // class files in a temporary directory. The source files have compilation // dependencies to all Jar files in /home/me/myproject/lib and to the Jar file // /home/me/myproject/optlib/opt.jar // A temporary java.io.File directory for the compiled classes. This, along with // all of its contents, will be automatically deleted when Schmant exits (unless // the -k flag was used when Schmant was launched). ctarget = TempFileUtil.createTempDir(); // This is the lib directory expressed as a (read only) EntityFS Directory. // By using a Directory, the script can use the utility methods of // Directories to extract the files it wants. See below. libDir = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/lib"), true); // Get all jar files from the lib directory. The jar files are returned as a // Set of EntityFS EFile:s depjars = Directories.getAllFilesMatching(libDir, "*.jar"); // Add a dependency from the optlib directory. This is a Java File depjars.add(new File("/home/me/myproject/optlib/opt.jar")); // Compile the Java source files. new Jdk6JavacTF(). addSource(new File("/home/me/myproject/src")). addClasspathEntries(depjars). setTarget(ctarget).run(); // A timestamp for the built archive // java.text.SimpleDateFormat must be fully qualified since the java.text // package classes are not automatically imported by Schmant. The "Packages" // prefix is for telling JavaScript that it is a Java package. It is required // sometimes. See the chapter on script language support for more information. timestamp = new Packages.java.text.SimpleDateFormat("yyyyMMddHHmm"). format(new Date()); // Build the Jar file. Put it in /home/me/myproject new JarTF(). addSource(ctarget). setTarget(new File("/home/me/myproject/myproject" + timestamp + ".jar")). run(); enableTaskPackage("net.findbugs"); // Run Findbugs. new ExtFindbugsTF(). // If the findbugsExecutable property is not set, the task looks for the // findbugs executable by checking if the FINDBUGS_HOME environment variable // is set. If not, it searches for findbugs in the directories referenced by // the PATH environment variable. // setFindbugsExecutable("c:\\Java\\findbugs\\bin\\findbugs.bat"). // // Add classpath dependencies that should not be analyzed addAuxClasspathEntries(depjars). // Add all source files addSourceCodeContainer(new File("/home/me/myproject/src")). // The compiled classes are located under the directory ctarget (and // subdirectories) addSource(ctarget). setFindbugsReportFormat(FindbugsReportFormat.HTML). setTarget( new File("/home/me/myproject/myproject" + timestamp + ".html")).run();

JRuby

# Compile files from the directory /home/me/myproject/src and put the resulting # class files in a temporary directory. The source files have compilation # dependencies to all Jar files in /home/me/myproject/lib and to the Jar file # /home/me/myproject/optlib/opt.jar # A temporary java.io.File directory for the compiled classes. This, along with # all of its contents, will be automatically deleted when Schmant exits (unless # the -k flag was used when Schmant was launched). ctarget = Schmant::TempFileUtil.createTempDir # This is the lib directory expressed as a (read only) EntityFS Directory. # By using a Directory, the script can use the utility methods of # Directories to extract the files it wants. See below. # # File is not included in the Java module, so we have to access it through the # Java module instead. libDir = Schmant::SchmantFileSystems.getEntityForDirectory( Java::JavaIo::File.new("/home/me/myproject/lib"), true) # Get all jar files from the lib directory. The jar files are returned as a # Set of EntityFS EFile:s depjars = Schmant::Directories.getAllFilesMatching(libDir, "*.jar") # Add a dependency from the optlib directory. This is a Java File depjars.add(Java::JavaIo::File.new("/home/me/myproject/optlib/opt.jar")) # Compile the Java source files. Schmant::Jdk6JavacTF.new. addSource(Java::JavaIo::File.new("/home/me/myproject/src")). addClasspathEntries(depjars). setTarget(ctarget).run # A timestamp for the built archive timestamp = Java::JavaText::SimpleDateFormat.new("yyyyMMddHHmm"). format(Java::JavaUtil::Date.new) # Build the Jar file. Put it in /home/me/myproject Schmant::JarTF.new. addSource(ctarget). setTarget( Java::JavaIo::File.new("/home/me/myproject/myproject" + timestamp + ".jar")). run enableTaskPackage "net.findbugs" # Run Findbugs. Schmant::ExtFindbugsTF.new. # If the findbugsExecutable property is not set, the task looks for the # findbugs executable by checking if the FINDBUGS_HOME environment variable # is set. If not, it searches for findbugs in the directories referenced by # the PATH environment variable. # setFindbugsExecutable("c:\\Java\\findbugs\\bin\\findbugs.bat"). # # Add classpath dependencies that should not be analyzed addAuxClasspathEntries(depjars). # Add all source files addSourceCodeContainer(Java::JavaIo::File.new("/home/me/myproject/src")). # The compiled classes are located under the directory ctarget (and # subdirectories) addSource(ctarget). setFindbugsReportFormat(Schmant::FindbugsReportFormat::HTML). setTarget( Java::JavaIo::File.new("/home/me/myproject/myproject" + timestamp + ".html")). run

Jython

# Compile files from the directory /home/me/myproject/src and put the resulting # class files in a temporary directory. The source files have compilation # dependencies to all Jar files in /home/me/myproject/lib and to the Jar file # /home/me/myproject/optlib/opt.jar # A temporary java.io.File directory for the compiled classes. This, along with # all of its contents, will be automatically deleted when Schmant exits (unless # the -k flag was used when Schmant was launched). ctarget = TempFileUtil.createTempDir() # This is the lib directory expressed as a (read only) EntityFS Directory. # By using a Directory, the script can use the utility methods of # Directories to extract the files it wants. See below. libDir = SchmantFileSystems.getEntityForDirectory( File("/home/me/myproject/lib"), True) # Get all jar files from the lib directory. The jar files are returned as a # Set of EntityFS EFile:s depjars = Directories.getAllFilesMatching(libDir, "*.jar") # Add a dependency from the optlib directory. This is a Java File depjars.add(File("/home/me/myproject/optlib/opt.jar")) # Compile the Java source files. Jdk6JavacTF(). \ addSource(File("/home/me/myproject/src")). \ addClasspathEntries(depjars). \ setTarget(ctarget).run() # A timestamp for the built archive # # SimpleDateFormat is not automatically imported by the preparation # script. from java.text import SimpleDateFormat timestamp = SimpleDateFormat("yyyyMMddHHmm"). \ format(Date()) # Build the Jar file. Put it in /home/me/myproject JarTF(). \ addSource(ctarget). \ setTarget(File("/home/me/myproject/myproject" + timestamp + ".jar")). \ run() enableTaskPackage("net.findbugs") # Run Findbugs. # # If the findbugsExecutable property is not set, the task looks for the # findbugs executable by checking if the FINDBUGS_HOME environment variable is # set. If not, it searches for findbugs in the directories referenced by the # PATH environment variable. # # The compiled classes are located under the directory ctarget (and # subdirectories) ExtFindbugsTF(). \ addAuxClasspathEntries(depjars). \ addSourceCodeContainer(File("/home/me/myproject/src")). \ addSource(ctarget). \ setFindbugsReportFormat(FindbugsReportFormat.HTML). \ setTarget( \ File("/home/me/myproject/myproject" + timestamp + ".html")).run()

If a script uses task packages containing tasks with conflicting library dependencies, Schmant can be run with the classloader argument -c isolated, giving all task packages their own java.lang.ClassLoader:s. See Chapter 9, Advanced topics.

The task packages that are included with Schmant, are listed in the Task package index. The Task Author's Guide contains information on how to create new task packages.

Modern computers with multiple processor cores, hyperthreading and whatnot are designed for running several threads in parallel. It would be a shame to let all that processing power be wasted by running just single threaded builds, wouldn't it? Using several build threads may even give a speed boost on a uniprocessor system since different tasks have different performance profiles – some tasks are CPU bound, others are I/O bound.

Schmant comes with the TaskExecutor for distributing task execution over a collection of threads. Tasks are added to the task executor and it executes them in parallel using a fixed number of threads from a thread pool.

This performance boost does come with added complexity, though. When parallelizing tasks, dependencies between different tasks have to be dealt with; Task B may need the results from Task A to run. In Schmant, that task B is dependent on task A is expressed by adding an object implementing the TaskDependency interface together with B to the task executor. Task, but not TaskFactory, does that, so A can be used to express the dependency when adding B.

A TaskExecutor is used by

  1. Creating it.

  2. Adding tasks to it.

  3. Calling waitFor() on it to wait for all tasks to be run.

  4. Calling shutdown() to release all of its resources.

A script can also call waitFor(TaskDependency) to wait for a specific dependency to be met.

Example 7.1. Compile Java files and build a Jar file, using a task executor

Groovy

import java.io.File import java.text.SimpleDateFormat import java.util.Date import org.entityfs.util.* import org.entityfs.util.filter.entity.* import org.schmant.run.TaskExecutor import org.schmant.support.entityfs.* import org.schmant.support.io.* import org.schmant.task.io.TreeCopyTF import org.schmant.task.jdk.jar.JarTF import org.schmant.task.jdk.javac.jdk6.Jdk6JavacTF // A temporary directory for the compiled classes. This, along with all of its // contents, will be automatically deleted when Schmant exits (unless the -k // flag is used). def ctarget = TempFileUtil.createTempDir() // The lib directory that contains Jar files that the code to compile uses. def libDir = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/lib"), true) // Get all jar files from the lib directory. The jar files are returned as a // Set of EntityFS EFile:s def depjars = Directories.getAllFilesMatching(libDir, "*.jar") // Add a dependency from the optlib directory. This is a Java File depjars.add(new File("/home/me/myproject/optlib/opt.jar")) // Create a read only, locking Directory object for the source directory. // A locking file system eliminates the risk of accidental concurrent // modification of files by parallel build threads. def src = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/src"), true) // Create a task executor that, by default, will use two build threads, unless // the noOfBuildThreads property is set to another value. def te = new TaskExecutor(). setNumberOfThreads(props.getIntValue("noOfBuildThreads", 2)). start() try { // A task for compiling all source files // TaskExecutor.add returns a TaskDependency object for the added task. def javacDep = te.add( new Jdk6JavacTF(). addSource(src). addClasspathEntries(depjars). setTarget(ctarget)) // A task for copying all non-Java files from the source hierarchy to the // target hierarchy. def copyDep = te.add( new TreeCopyTF(). // Create a view of the source hierarchy that hides all .java files. setSource( src.newView( // Groovy lets us use the ~ operator to negate the filter. ~ new EFileNameExtensionFilter("java"))). setTarget(ctarget)) // A timestamp for the built archive def timestamp = new SimpleDateFormat("yyyyMMddHHmm"). format(new Date()) // Build the Jar file. Put it in /home/me/myproject // The Jar build task depends on both the compiling and the copying tasks. te.add( new JarTF(). addSource(ctarget). setTarget(new File("/home/me/myproject/myproject" + timestamp + ".jar")), // Use an array for the two dependencies. [javacDep, copyDep]) // Wait for all tasks to complete te.waitFor() } finally { te.shutdown() }

JavaScript

// A temporary directory for the compiled classes. This, along with all of its // contents, will be automatically deleted when Schmant exits (unless the -k // flag is used). ctarget = TempFileUtil.createTempDir(); // The lib directory that contains Jar files that the code to compile uses. libDir = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/lib"), true); // Get all jar files from the lib directory. The jar files are returned as a // Set of EntityFS EFile:s depjars = Directories.getAllFilesMatching(libDir, "*.jar"); // Add a dependency from the optlib directory. This is a Java File depjars.add(new File("/home/me/myproject/optlib/opt.jar")); // Create a read only, locking Directory object for the source directory. // A locking file system eliminates the risk of accidental concurrent // modification of files by parallel build threads. src = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/src"), true); // Create a task executor that, by default, will use two build threads, unless // the noOfBuildThreads property is set to another value. te = new TaskExecutor(). setNumberOfThreads(props.getIntValue("noOfBuildThreads", 2)). start(); try { // A task for compiling all source files // TaskExecutor.add returns a TaskDependency object for the added task. javacDep = te.add( new Jdk6JavacTF(). addSource(src). addClasspathEntries(depjars). setTarget(ctarget)); // A task for copying all non-Java files from the source hierarchy to the // target hierarchy. copyDep = te.add( new TreeCopyTF(). // Create a view of the source hierarchy that hides all .java files. setSource( src.newView( new EFileNameExtensionFilter("java").not())). setTarget(ctarget)); // A timestamp for the built archive timestamp = new Packages.java.text.SimpleDateFormat("yyyyMMddHHmm"). format(new Date()); // Build the Jar file. Put it in /home/me/myproject // The Jar build task depends on both the compiling and the copying tasks. te.add( new JarTF(). addSource(ctarget). setTarget(new File("/home/me/myproject/myproject" + timestamp + ".jar")), // Use an array for the two dependencies [javacDep, copyDep]); // Wait for all tasks to complete te.waitFor(); } finally { te.shutdown(); }

JRuby

# A temporary directory for the compiled classes. This, along with all of its # contents, will be automatically deleted when Schmant exits (unless the -k # flag is used). ctarget = Schmant::TempFileUtil.createTempDir # The lib directory that contains Jar files that the code to compile uses. libDir = Schmant::SchmantFileSystems.getEntityForDirectory( Java::JavaIo::File.new("/home/me/myproject/lib"), true) # Get all jar files from the lib directory. The jar files are returned as a # Set of EntityFS EFile:s depjars = Schmant::Directories.getAllFilesMatching(libDir, "*.jar") # Add a dependency from the optlib directory. This is a Java File depjars.add Java::JavaIo::File.new("/home/me/myproject/optlib/opt.jar") # Create a read only, locking Directory object for the source directory. # A locking file system eliminates the risk of accidental concurrent # modification of files by parallel build threads. src = Schmant::SchmantFileSystems.getEntityForDirectory( Java::JavaIo::File.new("/home/me/myproject/src"), true) # Create a task executor that, by default, will use two build threads, unless # the noOfBuildThreads property is set to another value. te = Schmant::TaskExecutor.new. setNumberOfThreads($props.getIntValue("noOfBuildThreads", 2)). start begin # A task for compiling all source files # TaskExecutor.add returns a TaskDependency object for the added task. javacDep = te.add( Schmant::Jdk6JavacTF.new. addSource(src). addClasspathEntries(depjars). setTarget(ctarget)) # A task for copying all non-Java files from the source hierarchy to the # target hierarchy. copyDep = te.add( Schmant::TreeCopyTF.new. # Create a view of the source hierarchy that hides all .java files. setSource( src.newView( Schmant::EFileNameExtensionFilter.new("java").not)). setTarget(ctarget)) # A timestamp for the built archive # SimpleDateFormat and Date are not included in the Schmant module, so we have # to access them through the Java module instead. ts = Java::JavaText::SimpleDateFormat.new("yyyyMMddHHmm"). format(Java::JavaUtil::Date.new) # Build the Jar file. Put it in /home/me/myproject # The Jar build task depends on both the compiling and the copying tasks. te.add( Schmant::JarTF.new. addSource(ctarget). setTarget( Java::JavaIo::File.new("/home/me/myproject/myproject" + ts + ".jar")), # Use an array for the two dependencies [javacDep, copyDep]) # Wait for all tasks to complete te.waitFor ensure te.shutdown end

Jython

# A temporary directory for the compiled classes. This, along with all of its # contents, will be automatically deleted when Schmant exits (unless the -k # flag is used). ctarget = TempFileUtil.createTempDir() # The lib directory that contains Jar files that the code to compile uses. libDir = SchmantFileSystems.getEntityForDirectory( File("/home/me/myproject/lib"), True) # Get all jar files from the lib directory. The jar files are returned as a # Set of EntityFS EFile:s depjars = Directories.getAllFilesMatching(libDir, "*.jar") # Add a dependency from the optlib directory. This is a Java File depjars.add(File("/home/me/myproject/optlib/opt.jar")) # Create a read only, locking Directory object for the source directory. # A locking file system eliminates the risk of accidental concurrent # modification of files by parallel build threads. src = SchmantFileSystems.getEntityForDirectory( File("/home/me/myproject/src"), \ True) # Create a task executor that, by default, will use two build threads, unless # the noOfBuildThreads property is set to another value. te = TaskExecutor(). \ setNumberOfThreads(props.getIntValue("noOfBuildThreads", 2)). \ start() try: # A task for compiling all source files # TaskExecutor.add returns a TaskDependency object for the added task. javacDep = te.add( Jdk6JavacTF(). \ addSource(src). \ addClasspathEntries(depjars). \ setTarget(ctarget)) # A task for copying all non-Java files from the source hierarchy to the # target hierarchy. # # Create a view of the source hierarchy that hides all .java files. copyDep = te.add( TreeCopyTF(). \ setSource( src.newView( EFileNameExtensionFilter("java").not())). \ setTarget(ctarget)) # A timestamp for the built archive # # SimpleDateFormat is not automatically imported by the preparation # script. from java.text import SimpleDateFormat timestamp = SimpleDateFormat("yyyyMMddHHmm"). \ format(Date()) # Build the Jar file. Put it in /home/me/myproject # The Jar build task depends on both the compiling and the copying # tasks. # # Use a list for the two dependencies te.add( JarTF(). \ addSource(ctarget). \ setTarget( File("/home/me/myproject/myproject" + timestamp + ".jar")), \ [javacDep, copyDep]) # Wait for all tasks to complete te.waitFor() finally: te.shutdown()

When scheduling a task that depends on the results from several other tasks, its dependencies can be given in any of the following ways:

  • By listing all dependencies on the add method's argument list. Since scripts cannot use variable length argument lists[1], all dependencies have to be listed in an array. In JavaScript, this is expressed as te.add(task, [dep1, dep2, …, depN]).

  • By passing in all dependencies in a Collection, such as an ArrayList.

  • By using an CompoundTaskDependency.

The task executor will behave in the same way regardless of the way that the dependencies are given. Just use the way that is most convenient for the task being scheduled.

Task factories may be reused to create several tasks:

Example 7.2. Reusing a task factory

Groovy

import org.schmant.run.TaskExecutor import org.schmant.task.jdk.java.JavaTF import org.schmant.task.jdk.javac.jdk6.Jdk6JavacTF // Create a task executor with two parallel build threads def te = new TaskExecutor(). setNumberOfThreads(2). start() try { // Compile the source files in the src directory (hierarchy) and put the // classes in bin javacTask = new Jdk6JavacTF(). setSource(src). setTarget(bin).create() te.add(javacTask) // Create a task factory for running a Java program tf = new JavaTF(). setClassToRun("Test1"). // This is not configurable. Use the global variable bin. addClasspathEntry(bin) // Configure the task factory for the first program run, create a task and add // it to the task executor te.add( tf.addJvmOption("-Dexample.config=first"), javacTask) // Configure the task factory for the second program run, create a task and // add it to the task executor te.add( tf.addJvmOption("-Dexample.config=second"), javacTask) // Wait for all tasks to finish te.waitFor() } finally { te.shutdown() }

JavaScript

// Create a task executor with two parallel build threads te = new TaskExecutor(). setNumberOfThreads(2). start(); try { // Compile the source files in the src directory (hierarchy) and put the // classes in bin javacTask = new Jdk6JavacTF(). setSource(src). setTarget(bin).create(); te.add(javacTask); // Create a task factory for running a Java program tf = new JavaTF(). setClassToRun("Test1"). // This is not configurable. Use the global variable bin. addClasspathEntry(bin); // Configure the task factory for the first program run, create a task and add // it to the task executor te.add( tf.addJvmOption("-Dexample.config=first"), javacTask); // Configure the task factory for the second program run, create a task and // add it to the task executor te.add( tf.addJvmOption("-Dexample.config=second"), javacTask); // Wait for all tasks to finish te.waitFor(); } finally { te.shutdown(); }

JRuby

# Create a task executor with two parallel build threads te = Schmant::TaskExecutor.new. setNumberOfThreads(2). start begin # Compile the source files in the src directory (hierarchy) and put the # classes in bin javacTask = Schmant::Jdk6JavacTF.new. setSource($src). setTarget($bin).create te.add(javacTask) # Create a task factory for running a Java program tf = Schmant::JavaTF.new. setClassToRun("Test1"). # This is not configurable. Use the global variable bin. addClasspathEntry($bin) # Configure the task factory for the first program run, create a task and add # it to the task executor te.add( tf.addJvmOption("-Dexample.config=first"), javacTask) # Configure the task factory for the second program run, create a task and # add it to the task executor te.add( tf.addJvmOption("-Dexample.config=second"), javacTask) # Wait for all tasks to finish te.waitFor ensure te.shutdown end

Jython

# Create a task executor with two parallel build threads te = TaskExecutor(). \ setNumberOfThreads(2). \ start() try: # Compile the source files in the src directory (hierarchy) and put the # classes in bin javacTask = Jdk6JavacTF(). \ setSource(src). \ setTarget(bin).create() te.add(javacTask) # Create a task factory for running a Java program # # Use the classpath entries from the global variable "bin" tf = JavaTF(). \ setClassToRun("Test1"). \ addClasspathEntry(bin) # Configure the task factory for the first program run, create a task and add # it to the task executor te.add( \ tf.addJvmOption("-Dexample.config=first"), \ javacTask) # Configure the task factory for the second program run, create a task and # add it to the task executor te.add( \ tf.addJvmOption("-Dexample.config=second"), \ javacTask) # Wait for all tasks to finish te.waitFor() finally: te.shutdown()

Instead of registering a task with a TaskExecutor, a script may register a closure instead. A closure is a block of code that has access to the variables of the surrounding scope, but that may be executed at a later time. See Wikipedia article on closure.

Example 7.3. Running a closure with a task executor

Groovy

// enableTaskPackage org.at4j import org.entityfs.util.* import org.schmant.run.TaskExecutor import org.schmant.support.FutureFile import org.schmant.task.at4j.tar.TarTF // After tar:ing the directory d to a file in the directory targetD, create an // info file containing the size of the tar file. def te = new TaskExecutor(). start() try { def targetFile = new FutureFile(targetD, "d.tar") def tarTask = new TarTF(). setSource(d). setTarget(targetFile).create() te.add(tarTask) // Add a closure for creating the information file. This closure does not have // any arguments, but it has access to the variables in the scope where it is // defined. // Note the curly braces. te.add( { def sz = Files.getSize(targetFile.file) def infoFile = Directories.newFile(targetD, "d.txt") Files.writeText(infoFile, "" + sz) }, // The tar task must be run before the closure. tarTask) te.waitFor() } finally { te.shutdown() }

JavaScript

// After tar:ing the directory d to a file in the directory targetD, create an // info file containing the size of the tar file. enableTaskPackage("org.at4j"); te = new TaskExecutor(). start(); try { targetFile = new FutureFile(targetD, "d.tar"); tarTask = new TarTF(). setSource(d). setTarget(targetFile).create(); te.add(tarTask); // Add a closure for creating the information file. This closure does not have // any arguments, but it has access to the variables in the scope where it is // defined. te.add(function() { var sz = Files.getSize(targetFile.getFile()); var infoFile = Directories.newFile(targetD, "d.txt"); Files.writeText(infoFile, "" + sz); }, // The tar task must be run before the closure. tarTask); te.waitFor(); } finally { te.shutdown(); }

JRuby

# After tar:ing the directory d to a file in the directory targetD, create an # info file containing the size of the tar file. enableTaskPackage "org.at4j" te = Schmant::TaskExecutor.new. start(); begin targetFile = Schmant::FutureFile.new($targetD, "d.tar") tarTask = Schmant::TarTF.new. setSource($d). setTarget(targetFile).create te.add tarTask # Bind the global variable to a variable defined in this scope. The closure # does not have access to the global variable. targetDir = $targetD # Add a closure for creating the information file. This closure does not have # any arguments, but it has access to the variables from the scope where it is # defined. te.add( Proc.new { sz = Schmant::Files.getSize targetFile.file infoFile = Schmant::Directories.newFile(targetDir, "d.txt") Schmant::Files.writeText(infoFile, sz.to_s) }, # The tar task must be run before the closure. tarTask) te.waitFor ensure te.shutdown end

Jython

# After tar:ing the directory d to a file in the directory targetD, create an # info file containing the size of the tar file. enableTaskPackage("org.at4j") te = TaskExecutor().start() try: targetFile = FutureFile(targetD, "d.tar") tarTask = TarTF(). \ setSource(d). \ setTarget(targetFile).create() te.add(tarTask) # Jython does not support closures. Function variables and lambda functions # may be used instead # # The function that will create the info file. It uses global variables and # the targetFile variable that is defined in this scope. def createInfoFile(): sz = Files.getSize(targetFile.getFile()) infoFile = Directories.newFile(targetD, "d.txt") Files.writeText(infoFile, str(sz)) # Add the function that creates the info file to the task executor. It must be # run after the tar task. # # The function cannot take any arguments. te.add(createInfoFile, tarTask) te.waitFor() finally: te.shutdown()

Closures may be used in several places in Schmant, for instance:

The next example shows how a closure may be used instead of a task factory when running a RecursiveActionTF.

Example 7.4. Using a closure instead of a task factory

Groovy

// enableTaskPackage org.at4j import org.at4j.comp.CompressionLevel import org.entityfs.util.* import org.entityfs.util.filter.entity.EFileNameExtensionFilter import org.schmant.arg.DirectoryAndFilter import org.schmant.run.TaskExecutor import org.schmant.support.FutureFile import org.schmant.task.at4j.bzip2.BZip2TF import org.schmant.task.meta.RecursiveActionTF // For each text file in the directory hierarchy under the directory d, bzip2 it // with medium compression if it is less than 4096 bytes, and with high // compression if it is larger // Use this task executor to run the tasks. def te = new TaskExecutor(). setNumberOfThreads(2). start() try { // This constant will be used in the closure, which demonstrates that the // closure has access to the surrounding scope def MIN_SIZE_FOR_MAX_COMPRESSION = 4096 // bytes new RecursiveActionTF(). // Use a filter that will only return text files setSource(new DirectoryAndFilter( d, new EFileNameExtensionFilter(".txt"))). // Instead of running the entire RecursiveActionTF in the task executor, use // it to run each task that the RecursiveActionTF creates instead. setTaskExecutor(te). // Use a closure instead of a task. Since this is run in a // RecursiveActionTF, the closure will be called with a ClosureParameters // object containing a source entity. If the task would have been run in a // RecursiveProcessTF instead, the ClosureParameters object would // have contained both a source and a target entity. // // Note the curly braces setTaskFactory{ cParams -> def textFile = cParams.source def targetName = Entities.getName(textFile) + ".bz2" def targetDir = Entities.getParent(textFile) // Medium or high compression? def compressionLevel if (Files.getSize(textFile) < MIN_SIZE_FOR_MAX_COMPRESSION) { compressionLevel = CompressionLevel.DEFAULT } else { compressionLevel = CompressionLevel.BEST } new BZip2TF(). setSource(textFile). setTarget(new FutureFile(targetDir, targetName)). setCompressionLevel(compressionLevel).run() }.run() te.waitFor() } finally { te.shutdown() }

JavaScript

// For each text file in the directory hierarchy under the directory d, bzip2 it // with medium compression if it is less than 4096 bytes, and with high // compression if it is larger enableTaskPackage("org.at4j"); // Use this task executor to run the tasks. te = new TaskExecutor(). setNumberOfThreads(2). start(); try { // This constant will be used in the closure, which demonstrates that the // closure has access to the surrounding scope var MIN_SIZE_FOR_MAX_COMPRESSION = 4096; // bytes new RecursiveActionTF(). // Use a filter that will only return text files setSource(new DirectoryAndFilter( d, new EFileNameExtensionFilter(".txt"))). // Instead of running the entire RecursiveActionTF in the task executor, use // it to run each task that the RecursiveActionTF creates instead. setTaskExecutor(te). // Use a closure instead of a task. Since this is run in a // RecursiveActionTF, the closure will be called with a ClosureParameters // object containing a source entity. If the task would have been run in a // RecursiveProcessTF instead, the ClosureParameters object would // have contained both a source and a target entity. setTaskFactory(function(cParams) { var textFile = cParams.source; var targetName = Entities.getName(textFile) + ".bz2"; var targetDir = Entities.getParent(textFile); // Medium or high compression? if (Files.getSize(textFile) < MIN_SIZE_FOR_MAX_COMPRESSION) { var compressionLevel = CompressionLevel.DEFAULT; } else { var compressionLevel = CompressionLevel.BEST; } new BZip2TF(). setSource(textFile). setTarget(new FutureFile(targetDir, targetName)). setCompressionLevel(compressionLevel).run(); }).run(); te.waitFor(); } finally { te.shutdown(); }

JRuby

# For each text file in the directory hierarchy under the directory d, bzip2 it # with medium compression if it is less than 4096 bytes, and with high # compression if it is larger enableTaskPackage "org.at4j" # Use this task executor to run the tasks. te = Schmant::TaskExecutor.new. setNumberOfThreads(2). start begin # This constant will be used in the closure, which demonstrates that the # closure has access to the surrounding scope MIN_SIZE_FOR_MAX_COMPRESSION = 4096; # bytes Schmant::RecursiveActionTF.new. # Use a filter that will only return text files setSource(Schmant::DirectoryAndFilter.new( $d, Schmant::EFileNameExtensionFilter.new(".txt"))). # Instead of running the entire RecursiveActionTF in the task executor, use # it to run each task that the RecursiveActionTF creates instead. setTaskExecutor(te). # Use a closure instead of a task. Since this is run in a # RecursiveActionTF, the closure will be called with a ClosureParameters # object containing a source entity. If the task would have been run in a # RecursiveProcessTF instead, the ClosureParameters object would # have contained both a source and a target entity. setTaskFactory( Proc.new { |cParams| textFile = cParams.source targetName = Schmant::Entities.getName(textFile) + ".bz2" targetDir = Schmant::Entities.getParent(textFile) # Medium or high compression? if Schmant::Files.getSize(textFile) < MIN_SIZE_FOR_MAX_COMPRESSION compressionLevel = Schmant::CompressionLevel::DEFAULT else compressionLevel = Schmant::CompressionLevel::BEST end Schmant::BZip2TF.new. setSource(textFile). setTarget(Schmant::FutureFile.new(targetDir, targetName)). setCompressionLevel(compressionLevel).run }).run te.waitFor ensure te.shutdown end

Jython

# For each text file in the directory hierarchy under the directory d, bzip2 it # with medium compression if it is less than 4096 bytes, and with high # compression if it is larger # Jython does not support closures. The closest that we can get is using lambda # expressions or function variables # # Define the function that will be called by the task def compressFile(cParams): textFile = cParams.getSource() targetName = Entities.getName(textFile) + ".bz2" targetDir = Entities.getParent(textFile) # Medium or high compression? if Files.getSize(textFile) < MIN_SIZE_FOR_MAX_COMPRESSION: compressionLevel = CompressionLevel.DEFAULT else: compressionLevel = CompressionLevel.BEST BZip2TF(). \ setSource(textFile). \ setTarget(FutureFile(targetDir, targetName)). \ setCompressionLevel(compressionLevel).run() enableTaskPackage("org.at4j") # Use this task executor to run the tasks. te = TaskExecutor(). \ setNumberOfThreads(2). \ start() try: # This constant will be used in the closure, which demonstrates that the # closure has access to the surrounding scope MIN_SIZE_FOR_MAX_COMPRESSION = 4096; # bytes # Create the recursive action task # * Use a filter that will only return text files # * Instead of running the entire RecursiveActionTF in the task executor, use # it to run each task that the RecursiveActionTF creates instead. # * Use a function variable instead of a task. Since this is run in a # RecursiveActionTF, the function will be called with a ClosureParameters # object containing a source entity. If the task would have been run in a # RecursiveProcessTF instead, the ClosureParameters object would # have contained both a source and a target entity. RecursiveActionTF(). \ setSource(DirectoryAndFilter( \ d, \ EFileNameExtensionFilter(".txt"))). \ setTaskExecutor(te). \ setTaskFactory(compressFile).run() te.waitFor() finally: te.shutdown()

Some tasks such as the RecursiveActionTF or the JavaWorkspaceBuilderTF create and schedule their own tasks when they are run. When using such a task as a task dependency, the subtasks are not included in that dependency, and that is probably not what the programmer intended. Use the dependency from the task's getDependencyForTasksScheduledByThisTask method instead. That dependency will not be satisfied until all scheduled tasks have been run.

Example 7.5. Dependencies for task with subtasks

Groovy

// enableTaskPackage org.at4j import org.entityfs.util.Entities import org.schmant.run.TaskExecutor import org.schmant.support.FutureFile import org.schmant.task.at4j.tar.TarTF import org.schmant.task.io.gzip.GZipTF import org.schmant.task.meta.RecursiveActionTF // Gzip all files in a directory hierarchy under d, and then build a tar archive // containing all files. The tar archive is put in the directory targetD. // A task executor def te = new TaskExecutor().start() try { gzipTask = new RecursiveActionTF(). setSource(d). // Let the recursive action task schedule all tasks that it creates instead // of running them right away. setTaskExecutor(te). // Use a closure that compresses a file and then deletes the source file. // Note the curly braces. setTaskFactory{ cParams -> def targetName = Entities.getName(cParams.source) + ".gz" def targetDir = Entities.getParent(cParams.source) new GZipTF(). setSource(cParams.source). setTarget(new FutureFile(targetDir, targetName)). run() // Delete the original file. Entities.delete(cParams.source) // Run the recursive action task now. This will make it schedule all of its // tasks in the task executor. }.run() tarTask = new TarTF(). setSource(d). setTarget(new FutureFile(targetD, "d.tar")). create() // The tar task depends on all tasks scheduled by the recursive action task // above te.add(tarTask, gzipTask.dependencyForTasksScheduledByThisTask) te.waitFor() } finally { te.shutdown() }

JavaScript

// Gzip all files in a directory hierarchy under d, and then build a tar archive // containing all files. The tar archive is put in the directory targetD. enableTaskPackage("org.at4j"); // A task executor te = new TaskExecutor().start() try { gzipTask = new RecursiveActionTF(). setSource(d). // Let the recursive action task schedule all tasks that it creates instead // of running them right away. setTaskExecutor(te). // Use a closure that compresses a file and then deletes the source file. setTaskFactory( function(cParams) { var targetName = Entities.getName(cParams.source) + ".gz"; var targetDir = Entities.getParent(cParams.source); new GZipTF(). setSource(cParams.source). setTarget(new FutureFile(targetDir, targetName)). run(); // Delete the original file. // "delete" is a reserved word in JavaScript. Use the deleteEntity // method instead. Entities.deleteEntity(cParams.source); } // Run the recursive action task now. This will make it schedule all of its // tasks in the task executor. ).run(); tarTask = new TarTF(). setSource(d). setTarget(new FutureFile(targetD, "d.tar")). create(); // The tar task depends on all tasks scheduled by the recursive action task // above te.add(tarTask, gzipTask.getDependencyForTasksScheduledByThisTask()); te.waitFor(); } finally { te.shutdown(); }

JRuby

# Gzip all files in a directory hierarchy under d, and then build a tar archive # containing all files. The tar archive is put in the directory targetD. enableTaskPackage "org.at4j" # A task executor te = Schmant::TaskExecutor.new.start begin gzipTask = Schmant::RecursiveActionTF.new. setSource($d). # Let the recursive action task schedule all tasks that it creates instead # of running them right away. setTaskExecutor(te). # Use a closure that compresses a file and then deletes the source file. setTaskFactory( Proc.new { |cParams| targetName = Schmant::Entities.getName(cParams.source) + ".gz" targetDir = Schmant::Entities.getParent(cParams.source) Schmant::GZipTF.new. setSource(cParams.source). setTarget(Schmant::FutureFile.new(targetDir, targetName)). run # Delete the original file. Schmant::Entities.delete cParams.source } # Run the recursive action task now. This will make it schedule all of its # tasks in the task executor. ).run tarTask = Schmant::TarTF.new. setSource($d). setTarget(Schmant::FutureFile.new($targetD, "d.tar")). create # The tar task depends on all tasks scheduled by the recursive action task # above te.add(tarTask, gzipTask.dependencyForTasksScheduledByThisTask) te.waitFor ensure te.shutdown end

Jython

# Gzip all files in a directory hierarchy under d, and then build a tar archive # containing all files. The tar archive is put in the directory targetD. enableTaskPackage("org.at4j") # A task executor te = TaskExecutor().start() try: # The function that will be used for compressing the file def gzipFile(cParams): targetName = Entities.getName(cParams.source) + ".gz" targetDir = Entities.getParent(cParams.source) GZipTF(). \ setSource(cParams.source). \ setTarget(FutureFile(targetDir, targetName)). \ run() # Delete the original file. Entities.delete(cParams.source) # Create the recursive gzip task # * Let the recursive action task schedule all tasks that it creates instead # of running them right away. # * Use a closure that compresses a file and then deletes the source file. # * Run the recursive action task now. This will make it schedule all of its # tasks in the task executor. gzipTask = RecursiveActionTF(). \ setSource(d). \ setTaskExecutor(te). \ setTaskFactory(gzipFile).run() tarTask = TarTF(). \ setSource(d). \ setTarget(FutureFile(targetD, "d.tar")). \ create() # The tar task depends on all tasks scheduled by the recursive action task # above te.add(tarTask, gzipTask.getDependencyForTasksScheduledByThisTask()) te.waitFor() finally: te.shutdown()

A FutureEntity such as a FutureFile is used for representing an entity that does not yet exist when scheduling a task. See below for a simple example that uses a FutureFile to represent a file created by one task and then used by another.

Example 7.6. Using future files to represent files that do not yet exist

Groovy

import org.entityfs.util.Directories import org.schmant.support.FutureFile import org.schmant.task.io.gzip.GZipTF import org.schmant.task.text.TextReplaceTF // te is a task executor. // d is a Directory where the file f.txt to be processed is. // Note: in this example, we could have fed the result from TextReplaceTF right // into GZipTF's source property instead since TextReplaceTF is a Producer. // A FutureFile representing the result of the text processing task // created below. def fp = new FutureFile(d, "fp.txt") // Create a task for processing the template file f.txt def pt = new TextReplaceTF(). setSource(Directories.getFile(d, "f.txt")). setTarget(fp). addReplace("!!!VERSION!!!", "1.0").create() te.add(pt) // Create a task for GZip'ping the processed file te.add( new GZipTF(). // Reuse the future file, this time as the source parameter. Since this task // depends on the task creating the file, the future file will exist when // this task is run. setSource(fp). setTarget(new FutureFile(d, "fp.txt.gz")), // This task must depend on the processing task pt)

JavaScript

// te is a task executor. // d is a Directory where the file f.txt to be processed is. // Note: in this example, we could have fed the result from TextReplaceTF right // into GZipTF's source property instead since TextReplaceTF is a Producer. // A FutureFile representing the result of the text processing task // created below. fp = new FutureFile(d, "fp.txt"); // Create a task for processing the template file f.txt pt = new TextReplaceTF(). setSource(Directories.getFile(d, "f.txt")). setTarget(fp). addReplace("!!!VERSION!!!", "1.0").create(); te.add(pt); // Create a task for GZip'ping the processed file te.add( new GZipTF(). // Reuse the future file, this time as the source parameter. Since this task // depends on the task creating the file, the file will exist when this task // is run. setSource(fp). setTarget(new FutureFile(d, "fp.txt.gz")), // This task must depend on the processing task pt);

JRuby

# te is a task executor. # d is a Directory where the file f.txt to be processed is. # Note: in this example, we could have fed the result from TextReplaceTF right # into GZipTF's source property instead since TextReplaceTF is a Producer. # A FutureFile representing the result of the text processing task # created below. fp = FutureFile(d, "fp.txt") # Create a task for processing the template file f.txt pt = TextReplaceTF(). \ setSource(Directories.getFile(d, "f.txt")). \ setTarget(fp). \ addReplace("!!!VERSION!!!", "1.0").create() te.add(pt) # Create a task for GZip'ping the processed file. # This task must depend on the processing task. # Reuse the future file, this time as the source parameter. Since this task # depends on the task that creates the file, the file will exist when this task # is run. te.add( \ GZipTF(). \ setSource(fp). \ setTarget(FutureFile(d, "fp.txt.gz")), \ pt)

Jython

# te is a task executor. # d is a Directory where the file f.txt to be processed is. # Note: in this example, we could have fed the result from TextReplaceTF right # into GZipTF's source property instead since TextReplaceTF is a Producer. # A FutureFile representing the result of the text processing task # created below. fp = Schmant::FutureFile.new($d, "fp.txt") # Create a task for processing the template file f.txt pt = Schmant::TextReplaceTF.new. setSource(Schmant::Directories.getFile($d, "f.txt")). setTarget(fp). addReplace("!!!VERSION!!!", "1.0").create() $te.add(pt) # Create a task for GZip'ping the processed file $te.add( Schmant::GZipTF.new. # Reuse the future file, this time as the source parameter. Since this task # depends on the task creating the file, the file will exist when this task # is run. setSource(fp). setTarget(FutureFile.new($d, "fp.txt.gz")), # This task must depend on the processing task pt)

In the example below, an XML file is first preprocessed, then parsed and then "statistics" is created for it. The XML parser uses an XML catalog for resolving external entities. All tasks are run with a TaskExecutor[2].

Example 7.7. Computing statistics for an XML file

Groovy

import java.io.File import org.entityfs.util.* import org.schmant.run.TaskExecutor import org.schmant.support.* import org.schmant.support.entityfs.* import org.schmant.support.xml.* import org.schmant.task.meta.RecursiveActionTF import org.schmant.task.proxy.ReplaceSourceFileTF import org.schmant.task.script.ScriptTF import org.schmant.task.text.TextReplaceTF import org.schmant.task.xml.catalog.AddSystemIdToCatalogTF import org.schmant.task.xml.dom.DomParseXmlTF // Create a task executor with two threads and start it def te = new TaskExecutor(). setNumberOfThreads(2). start() try { // An XML catalog to use for resolving external entities def cr = new XmlCatalogResolver() // Add all DTD files in the directory DTD to the catalog def buildCatalogTask = new RecursiveActionTF(). setSource(dtd). setTaskFactory( new AddSystemIdToCatalogTF(). setXmlCatalog(cr)).create() // Add the task to the executor te.add buildCatalogTask // Create a FutureFile representing the file f. This is // done because the preprocessing task below will invalidate f by deleting it. // This is how EntityFS entities work; even though the preprocess task will // put a new file in the same location as f, that new file is not f. def ff = new FutureFile(f) // The XML document is in the EFile f. Preprocess it. // This makes the f variable invalid since the original file is replaced. def preprocessTask = new ReplaceSourceFileTF(). setSource(f). setTaskFactory( new TextReplaceTF(). addReplace("!!!VERSION!!!", "1.0")).create() // Add the task to the executor. This can be run in parallel with the XML // catalog task defined above. te.add preprocessTask // Define the XML parsing task. def parseTask = new DomParseXmlTF(). setSource(ff). setEntityResolver(cr).create() // Schedule the task and inform the task executor that it depends on the // preprocessing task and the XML catalog task. te.add(parseTask, [preprocessTask, buildCatalogTask]) // Write the XML "statistics" to this file def targetf = new File(props.getStringValue("java.io.tmpdir"), "out.txt") targetf.createNewFile() // Add a closure that calculates statistics from the XML file. Adding the // closure to the task executor will make it execute first when all of its // dependencies have executed. te.add( { // The closure does not have any arguments // // The closure has access to the variables in the surrounding scope. // Convert the target File targetf to an EFile def target = SchmantFileSystems.getEntityForFile(targetf, false) // Write the text to the target file // The parse task is a Producer of a Document object. Files.writeText(target, parseTask.get().toString()) }, // This depends on the parse task parseTask) // Wait for the task executor to finish te.waitFor() } finally { // Make sure that the task executor is stopped. te.shutdown() }

JavaScript

// Create a task executor with two threads and start it te = new TaskExecutor(). setNumberOfThreads(2). start(); try { // An XML catalog to use for resolving external entities cr = new XmlCatalogResolver(); // Add all DTD files in the directory DTD to the catalog buildCatalogTask = new RecursiveActionTF(). setSource(dtd). setTaskFactory( new AddSystemIdToCatalogTF(). setXmlCatalog(cr)).create(); // Add the task to the executor te.add(buildCatalogTask); // Create a FutureFile representing the file f. This is done because // the preprocessing task below will invalidate f by deleting it. This is how // EntityFS entities work; even though the preprocess task will put a new file // in the same location as f, that new file is not f. ff = new FutureFile(f); // The XML document is in the EFile f. Preprocess it. // This makes the f variable invalid since the original file is replaced. preprocessTask = new ReplaceSourceFileTF(). setSource(f). setTaskFactory( new TextReplaceTF(). addReplace("!!!VERSION!!!", "1.0")).create(); // Add the task to the executor. This can be run in parallel with the XML // catalog task defined above. te.add(preprocessTask); // Define the XML parsing task. parseTask = new DomParseXmlTF(). setSource(ff). setEntityResolver(cr).create(); // Schedule the task and inform the task executor that it depends on the // preprocessing task and the XML catalog task. te.add(parseTask, [preprocessTask, buildCatalogTask]); // Write the XML "statistics" to this file targetf = new File(props.getStringValue("java.io.tmpdir"), "out.txt"); targetf.createNewFile(); // Add a closure that calculates statistics from the XML file. Adding the // closure to the task executor will make it execute first when all of its // dependencies have executed. te.add( // The closure does not have any arguments function() { // The closure has access to the variables in the surrounding scope. // Convert the target File targetf to an EFile var target = SchmantFileSystems.getEntityForFile(targetf, false); // Write the text to the target file // The parse task is a Producer of a Document object. Files.writeText(target, parseTask.get().toString()); }, // This depends on the parse task. parseTask); // Wait for the task executor to finish te.waitFor(); } finally { // Make sure that the task executor is stopped. te.shutdown(); }

JRuby

# Create a task executor with two threads and start it te = Schmant::TaskExecutor.new. setNumberOfThreads(2). start begin # An XML catalog to use for resolving external entities cr = Schmant::XmlCatalogResolver.new # Add all DTD files in the directory DTD to the catalog buildCatalogTask = Schmant::RecursiveActionTF.new. setSource($dtd). setTaskFactory( Schmant::AddSystemIdToCatalogTF.new. setXmlCatalog(cr)).create # Add the task to the executor te.add buildCatalogTask # Create a FutureFile representing the file f. This is done because # the preprocessing task below will invalidate f by deleting it. This is how # EntityFS entities work; even though the preprocess task will put a new file # in the same location as f, that new file is not f. ff = Schmant::FutureFile.new $f # The XML document is in the EFile f. Preprocess it. # This makes the f variable invalid since the original file is replaced. preprocessTask = Schmant::ReplaceSourceFileTF.new. setSource($f). setTaskFactory( Schmant::TextReplaceTF.new. addReplace("!!!VERSION!!!", "1.0")).create # Add the task to the executor. This can be run in parallel with the XML # catalog task defined above. te.add preprocessTask # Define the XML parsing task. parseTask = Schmant::DomParseXmlTF.new. setSource(ff). setEntityResolver(cr).create # Schedule the task and inform the task executor that it depends on the # preprocessing task and the XML catalog task. te.add(parseTask, [preprocessTask, buildCatalogTask]) # Write the XML "statistics" to this file targetf = Java::JavaIo::File.new( $props.getStringValue("java.io.tmpdir"), "out.txt") targetf.createNewFile # Add a closure that calculates statistics from the XML file. Adding the # closure to the task executor will make it execute first when all of its # dependencies have executed. # # Create the closure using Proc.new. We cannot use a lambda expression here # since our closure spans several statements. te.add( Proc.new { || # The closure has access to the variables in the surrounding scope. # Convert the target File targetf to an EFile target = Schmant::SchmantFileSystems.getEntityForFile(targetf, false) # Write the "statistics" to the target file Schmant::Files.writeText(target, parseTask.get.toString) }, parseTask) # Wait for the task executor to finish te.waitFor ensure # Make sure that the task executor is stopped. te.shutdown end

Jython

# Create a task executor with two threads and start it te = TaskExecutor(). \ setNumberOfThreads(2). \ start() try: # An XML catalog to use for resolving external entities cr = XmlCatalogResolver() # Add all DTD files in the directory DTD to the catalog buildCatalogTask = RecursiveActionTF(). \ setSource(dtd). \ setTaskFactory( AddSystemIdToCatalogTF(). \ setXmlCatalog(cr)).create() # Add the task to the executor te.add(buildCatalogTask) # Create a FutureFile representing the file f. This is done because # the preprocessing task below will invalidate f by deleting it. This is # how EntityFS entities work; even though the preprocess task will put a # new file in the same location as f, that new file is not f. ff = FutureFile(f) # The XML document is in the EFile f. Preprocess it. # This makes the f variable invalid since the original file is replaced. preprocessTask = ReplaceSourceFileTF(). \ setSource(f). \ setTaskFactory( TextReplaceTF(). \ addReplace("!!!VERSION!!!", "1.0")).create() # Add the task to the executor. This can be run in parallel with the XML # catalog task defined above. te.add(preprocessTask) # Define the XML parsing task. parseTask = DomParseXmlTF(). \ setSource(ff). \ setEntityResolver(cr).create() # Schedule the task and inform the task executor that it depends on the # preprocessing task and the XML catalog task. te.add(parseTask, [preprocessTask, buildCatalogTask]) # Write the XML "statistics" to this file targetf = File(props.getStringValue("java.io.tmpdir"), "out.txt") targetf.createNewFile() # This function will be used for creating the XML "statistics". Since it # spans more than one statement, we cannot simply use a lambda for this def calculateXmlStatistics(): # This function has access to the variables in the surrounding scope. # Convert the target File targetf to an EFile. target = SchmantFileSystems.getEntityForFile(targetf, False) # Write the text to the target file # The parse task is a Producer of a Document object. Files.writeText(target, parseTask.get().toString()) # Add the function to the task executor. # It depends on the parse task te.add( calculateXmlStatistics, parseTask) # Wait for the task executor to finish te.waitFor() finally: # Make sure that the task executor is stopped. te.shutdown()

Java's File object works just like a FutureEntity object because, unlike EntityFS entities, it does not require that the file or directory that it references must exist.



[1] Varargs are Java syntactic sugar that are converted to arrays at compile time

[2] In this simple example, using a TaskExecutor does not give much of a performance boost since most tasks depend on each other.

A common scenario for a build script is to build and package a set of several interdependent source code modules such as different projects or modules in an IDE.

Schmant represents the collection of source code modules as a ProjectRepository that contains a collection of Project:s. There are two ProjectRepository implementations – EclipseWorkspace and IntelliJWorkspace. A Project may be a JavaProject or an IntelliJJavaProject.

The easiest way to build Java projects in a project repository is to use the JavaWorkspaceBuilderTF. It can build all or some of the projects in a ProjectRepository, and possibly also preprocess the source code before building and/or postprocess the class files after building.

The example below shows how all Java projects in an Eclipse workspace can be built.

Example 8.1. Build all projects in an Eclipse workspace using the workspace builder task.

Groovy

import org.schmant.project.eclipse.EclipseWorkspace import org.schmant.task.project.JavaWorkspaceBuilderTF // Create an EclipseWorkspace object for the workspace in the directory wos. // wos may be a File directory or an EntityFS Directory. def ewos = new EclipseWorkspace(wos) // ...if the workspace had been an IntelliJ IDEA project instead // def ewos = new IntelliJWorkspace( // wos, // new AbsoluteLocation("/MyProject.ipr")) // Create the build task and run it new JavaWorkspaceBuilderTF(). setWorkspace(ewos). // Put the classes in the directory bin setTarget(bin). run()

JavaScript

// Create an EclipseWorkspace object for the workspace in the directory wos. // wos may be a File directory or an EntityFS Directory. ewos = new EclipseWorkspace(wos); // ...if the workspace had been an IntelliJ IDEA project instead // ewos = new IntelliJWorkspace( // wos, // new AbsoluteLocation("/MyProject.ipr")); // Create the build task and run it new JavaWorkspaceBuilderTF(). setWorkspace(ewos). // Put the classes in the directory bin setTarget(bin). run();

JRuby

# Create an EclipseWorkspace object for the workspace in the directory wos. # wos may be a File directory or an EntityFS Directory. ewos = Schmant::EclipseWorkspace.new($wos) # ...if the workspace had been an IntelliJ IDEA project instead # ewos = IntelliJWorkspace.new( # $wos, # new AbsoluteLocation("/MyProject.ipr")) # Create the build task and run it Schmant::JavaWorkspaceBuilderTF.new. setWorkspace(ewos). # Put the classes in the directory bin setTarget($bin). run

Jython

# Create an EclipseWorkspace object for the workspace in the directory wos. # wos may be a File directory or an EntityFS Directory. ewos = EclipseWorkspace(wos) # ...if the workspace had been an IntelliJ IDEA project instead # ewos = IntelliJWorkspace( \ # wos, # AbsoluteLocation("/MyProject.ipr")) # Create the build task and run it. Put the compiled classes in the directory # bin. JavaWorkspaceBuilderTF(). \ setWorkspace(ewos). \ setTarget(bin). \ run()

The JavaWorkspaceBuilderTF automatically ignores all non-Java projects in the workspace. To further narrow down the selection of which projects to build, a Filter<Project> can be used with the task. There are a number of filter implementations bundled with Schmant:


All of Schmant's filter implementations also implement ConvenientFilter which adds convenience methods for combining different filters through logical operations such as AND or OR. All project filters also implement the marker interface ProjectFilter, which makes them easier to find in the API documentation.

The static methods in the ProjectFilterUtil class can be used for applying filters to collections of projects.

The functionality of JavaWorkspaceBuilderTF may not be enough for a build script. In that case, the build script can build a collection of projects manually. To do so, the script uses a ProjectDependencies object to sort out the TaskDependency:s between projects, and a TaskExecutor to schedule and run the build tasks. The ProjectDependencies object serves two purposes: it keeps track of a project's dependencies to other projects, and it is used to configure the build task for a project with the location of the built class files from each of the projects that it depends on.

The example below shows how all Java projects in an Eclipse workspace can be built manually.

Example 8.2. Build all projects in an Eclipse workspace manually

Groovy

import org.schmant.project.eclipse.EclipseWorkspace import org.schmant.project.java.* import org.schmant.run.TaskExecutor import org.schmant.task.jdk.javac.jdk6.Jdk6JavacTF // Create an EclipseWorkspace object for the workspace in the directory wos. // wos may be a File directory or an EntityFS Directory. def ewos = new EclipseWorkspace(wos) // Create a JavaProjectDependencies object to keep track of dependencies // between the projects in the workspace. def projDeps = new JavaProjectDependencies() // A TaskExecutor used for scheduling and running the compile tasks. def te = new TaskExecutor(). setNumberOfThreads(2). start() try { // Loop over all projects in the workspace. This assumes that all projects are // Java projects. See the next example for how to deal with non-Java projects. ewos.projects.each { proj -> def javacTask = new Jdk6JavacTF(). addSources(proj.sourceDirectories). setTarget(bin). addClasspathDecorator( // This classpath decorator is used to handle dependencies to the other // Java projects. It uses the JavaProjectDependencies object created // above. new JavaProjectClasspathDecorator(). setProject(proj). setDependencies(projDeps)).create() // Register this project's dependency with the dependency object. By doing // this, other projects that depend on this project will know when all their // dependencies are met. projDeps.registerDependency(proj, javacTask) // Register where the classes built from this project will be put. projDeps.registerClassDirectory(proj, bin) // Schedule this project for building. Get all dependencies for building // this project from the JavaProjectDependencies object. te.add(javacTask, projDeps.getDependencies(proj)) } te.waitFor() } finally { te.shutdown() }

JavaScript

// Create an EclipseWorkspace object for the workspace in the directory wos. // wos may be a File directory or an EntityFS Directory. ewos = new EclipseWorkspace(wos); // Create a JavaProjectDependencies object to keep track of dependencies // between the projects in the workspace. projDeps = new JavaProjectDependencies(); // A TaskExecutor used for scheduling and running the compile tasks. te = new TaskExecutor(). setNumberOfThreads(2). start(); try { // Get all projects from the workspace. This assumes that all projects are // Java projects. See the next example for how to deal with non-Java projects. itr = ewos.getProjects().iterator(); while(itr.hasNext()) { proj = itr.next(); javacTask = new Jdk6JavacTF(). addSources(proj.getSourceDirectories()). setTarget(bin). addClasspathDecorator( // This classpath decorator is used to handle dependencies to the other // Java projects. It uses the JavaProjectDependencies object created // above. new JavaProjectClasspathDecorator(). setProject(proj). setDependencies(projDeps)).create(); // Register this project's dependency with the dependency object. By doing // this, other projects that depend on this project will know when all their // dependencies are met. projDeps.registerDependency(proj, javacTask); // Register where the classes built from this project will be put. projDeps.registerClassDirectory(proj, bin); // Schedule this project for building. Get all dependencies for building // this project from the JavaProjectDependencies object. te.add(javacTask, projDeps.getDependencies(proj)); } te.waitFor(); } finally { te.shutdown(); }

JRuby

# Create an EclipseWorkspace object for the workspace in the directory wos. # wos may be a File directory or an EntityFS Directory. ewos = Schmant::EclipseWorkspace.new($wos) # Create a JavaProjectDependencies object to keep track of dependencies # between the projects in the workspace. projDeps = Schmant::JavaProjectDependencies.new # A TaskExecutor used for scheduling and running the compile tasks. te = Schmant::TaskExecutor.new. setNumberOfThreads(2). start begin # Get all projects from the workspace. This assumes that all projects are # Java projects. See the next example for how to deal with non-Java projects. ewos.projects.each do |proj| javacTask = Schmant::Jdk6JavacTF.new. addSources(proj.sourceDirectories). setTarget($bin). addClasspathDecorator( # This classpath decorator is used to handle dependencies to the other # Java projects. It uses the JavaProjectDependencies object created # above. Schmant::JavaProjectClasspathDecorator.new. setProject(proj). setDependencies(projDeps)).create() # Register this project's dependency with the dependency object. By doing # this, other projects that depend on this project will know when all their # dependencies are met. projDeps.registerDependency(proj, javacTask) # Register where the classes built from this project will be put. projDeps.registerClassDirectory(proj, $bin) # Schedule this project for building. Get all dependencies for building # this project from the JavaProjectDependencies object. te.add(javacTask, projDeps.getDependencies(proj)) end te.waitFor() ensure te.shutdown() end

Jython

# Create an EclipseWorkspace object for the workspace in the directory wos. # wos may be a File directory or an EntityFS Directory. ewos = EclipseWorkspace(wos) # Create a JavaProjectDependencies object to keep track of dependencies # between the projects in the workspace. projDeps = JavaProjectDependencies() # A TaskExecutor used for scheduling and running the compile tasks. te = TaskExecutor(). \ setNumberOfThreads(2). \ start() try: # Get all projects from the workspace. This assumes that all projects are # Java projects. See the next example for how to deal with non-Java projects. itr = ewos.getProjects().iterator(); while itr.hasNext(): proj = itr.next() # Create a compile task. # The classpath decorator is used to handle dependencies to the other Java # projects in the workspace. It uses the JavaProjectDependencies object # created above. javacTask = Jdk6JavacTF(). \ addSources(proj.getSourceDirectories()). \ setTarget(bin). \ addClasspathDecorator( \ JavaProjectClasspathDecorator(). \ setProject(proj). \ setDependencies(projDeps)).create() # Register this project's dependency with the dependency object. By doing # this, other projects that depend on this project will know when all their # dependencies are met. projDeps.registerDependency(proj, javacTask) # Register where the classes built from this project will be put. projDeps.registerClassDirectory(proj, bin) # Schedule this project for building. Get all dependencies for building # this project from the JavaProjectDependencies object. te.add(javacTask, projDeps.getDependencies(proj)) te.waitFor() finally: te.shutdown()

The example above made the assumption that all projects in the Eclipse workspace were Java projects, but of course this is not always the case. Use project filters like in the examples below to hide non-Java projects.

The EclipseWorkspace is used for representing an Eclipse workspace. It parses workspace and project information from the workspace and project configuration files, like a project's .project and .classpath files.

EclipseWorkspace supports Eclipse Java projects, classpath variables and user libraries. The workspace object tries to parse the values of classpath variables and user libraries from the workspace settings, if they are available. If the variable definitions are not available, or if the parsed values should be overridden, new values can be supplied in an EclipseWorkspaceSettings object instead when creating the workspace object.

EclipseWorkspace tries to create Project objects from all directories in the workspace directory. If there are project directories located elsewhere, the EclipseWorkspaceSettings object can be configured with their locations.

The examples below shows how all non-test Java projects in an Eclipse workspace can be built.

Example 8.3. Build a Jar file from the projects in an Eclipse workspace

Groovy

import java.io.File import org.schmant.project.eclipse.* import org.schmant.project.filter.ProjectNameGlobFilter import org.schmant.support.io.TempFileUtil import org.schmant.task.jdk.jar.JarTF import org.schmant.task.project.JavaWorkspaceBuilderTF // Create the Eclipse workspace object for the contents in the directory wos. // wos may be a File directory or an EntityFS Directory. // Create an EclipseWorkspaceSettings object that is used to override // information that is parsed from to the workspace configuration files. def settings = new EclipseWorkspaceSettings() // Add a classpath variable to the settings object. // The EclipseWorkspace object tries to parse the values of classpath // variables from the workspace metadata. If they are not defined there, or if // a value there should be overridden, classpath variable values may be added // manually to the EclipseWorkspaceSettings object. settings.addClasspathVariable("MY_VARIABLE", new File("lib/mylib.jar")) def eWos = new EclipseWorkspace(wos, settings) // To create an Eclipse workspace object for the contents in the Schmant // process' working directory, use // def eWos = new EclipseWorkspace(new File("."), settings) // Create a temporary directory for putting the class files in. This directory // will be removed when the Schmant process exits. def tmpDir = TempFileUtil.createTempDirectory() // To create a temporary directory in memory: // def tmpDir = SchmantFileSystems.createRamFileSystem() new JavaWorkspaceBuilderTF(). setWorkspace(eWos). setTarget(tmpDir). // Don't build test projects. Negate the filter using the ~ operator. setProjectFilter(~(new ProjectNameGlobFilter("*_test"))). run() // Build the Jar file // The target file targetFile may be a File or a FutureFile. new JarTF(). setSource(tmpDir). setTarget(targetFile).run()

JavaScript

// Create the Eclipse workspace object for the contents in the directory wos. // wos may be a File directory or an EntityFS Directory. // Create an EclipseWorkspaceSettings object that is used to override // information that is parsed from to the workspace configuration files. settings = new EclipseWorkspaceSettings(); // Add a classpath variable to the settings object. // The EclipseWorkspace object tries to parse the values of classpath // variables from the workspace metadata. If they are not defined there, or if // a value there should be overridden, classpath variable values may be added // manually to the EclipseWorkspaceSettings object. settings.addClasspathVariable("MY_VARIABLE", new File("lib/mylib.jar")); eWos = new EclipseWorkspace(wos, settings); // To create an Eclipse workspace object for the contents in the Schmant // process' working directory, use // eWos = new EclipseWorkspace(new File("."), settings); // Create a temporary directory for putting the class files in. This directory // will be removed when the Schmant process exits. tmpDir = TempFileUtil.createTempDirectory(); // To create a temporary directory in memory: // var tmpDir = SchmantFileSystems.createRamFileSystem(); new JavaWorkspaceBuilderTF(). setWorkspace(eWos). setTarget(tmpDir). // Don't build test projects. setProjectFilter(new ProjectNameGlobFilter("*_test").not()). run(); // Build the Jar file // The target file targetFile may be a File or a FutureFile. new JarTF(). setSource(tmpDir). setTarget(targetFile).run();

JRuby

# Create the Eclipse workspace object for the contents in the directory wos. # wos may be a File directory or an EntityFS Directory. # Create an EclipseWorkspaceSettings object that is used to override # information that is parsed from to the workspace configuration files. settings = Schmant::EclipseWorkspaceSettings.new # Add a classpath variable to the settings object. # The EclipseWorkspace object tries to parse the values of classpath # variables from the workspace metadata. If they are not defined there, or if # a value there should be overridden, classpath variable values may be added # manually to the EclipseWorkspaceSettings object. settings.addClasspathVariable( "MY_VARIABLE", Java::JavaIo::File.new("lib/mylib.jar")) eWos = Schmant::EclipseWorkspace.new($wos, settings) # To create an Eclipse workspace object for the contents in the Schmant # process' working directory, use # eWos = Schmant::EclipseWorkspace.new( # Java::JavaIo::File.new("."), # settings) # Create a temporary directory for putting the class files in. This directory # will be removed when the Schmant process exits. tmpDir = Schmant::TempFileUtil.createTempDirectory # To create a temporary directory in memory: # tmpDir = Schmant::SchmantFileSystems.createRamFileSystem Schmant::JavaWorkspaceBuilderTF.new. setWorkspace(eWos). setTarget(tmpDir). # Don't build test projects. setProjectFilter(Schmant::ProjectNameGlobFilter.new("*_test").not). run # Build the Jar file # The target file targetFile may be a File or a FutureFile. Schmant::JarTF.new. setSource(tmpDir). setTarget($targetFile).run

Jython

# Create the Eclipse workspace object for the contents in the directory wos. # wos may be a File directory or an EntityFS Directory. # Create an EclipseWorkspaceSettings object that is used to override # information that is parsed from to the workspace configuration files. settings = EclipseWorkspaceSettings() # Add a classpath variable to the settings object. # The EclipseWorkspace object tries to parse the values of classpath # variables from the workspace metadata. If they are not defined there, or if # a value there should be overridden, classpath variable values may be added # manually to the EclipseWorkspaceSettings object. settings.addClasspathVariable("MY_VARIABLE", File("lib/mylib.jar")) eWos = EclipseWorkspace(wos, settings) # To create an Eclipse workspace object for the contents in the Schmant # process' working directory, use # eWos = EclipseWorkspace(new File("."), settings) # Create a temporary directory for putting the class files in. This directory # will be removed when the Schmant process exits. tmpDir = TempFileUtil.createTempDirectory() # To create a temporary directory in memory: # var tmpDir = SchmantFileSystems.createRamFileSystem() JavaWorkspaceBuilderTF(). \ setWorkspace(eWos). \ setTarget(tmpDir). \ setProjectFilter(ProjectNameGlobFilter("*_test").not()). \ run() # Build the Jar file # The target file targetFile may be a File or a FutureFile. JarTF(). \ setSource(tmpDir). \ setTarget(targetFile).run()

Example 8.4. Build a Jar file from the projects in an Eclipse workspace manually

Groovy

import java.io.File import org.entityfs.util.Directories import org.schmant.project.eclipse.* import org.schmant.project.filter.ProjectNameGlobFilter import org.schmant.project.java.* import org.schmant.run.TaskExecutor import org.schmant.task.jdk.jar.JarTF import org.schmant.task.jdk.javac.jdk6.Jdk6JavacTF // Create the Eclipse workspace object for the contents in the directory wos. // wos may be a File directory or an EntityFS Directory. // Create an EclipseWorkspaceSettings object that is used to override // information that is parsed from the workspace configuration files. def settings = new EclipseWorkspaceSettings() // Add a classpath variable to the settings object. // The EclipseWorkspace object tries to parse the values of classpath // variables from the workspace metadata. If they are not defined there, or if // a value there should be overridden, classpath variable values may be added // manually to the EclipseWorkspaceSettings object. settings.addClasspathVariable("MY_VARIABLE", new File("lib/mylib.jar")) def eWos = new EclipseWorkspace(wos, settings) // To create an Eclipse workspace object for the contents in the Schmant // process' working directory, use // def eWos = new EclipseWorkspace(new File("."), settings) // Create a JavaProjectDependencies object for keeping track of dependencies // between different projects. def projDeps = new JavaProjectDependencies() // This collection will contain all the directories with class files def classDirs = [] // The tmpDir variable points to a directory where temporary files are kept // during the build. It can for instance be created by calling // def tmpDir = TempFileUtil.createTempDirectory() // Create a TaskExecutor and start it def te = new TaskExecutor(). setNumberOfThreads(2). start() try { // Get all non-test Java projects from the workspace // In Groovy, project filters may be combined using Groovy operators. def javaProjects = eWos.getProjects( JavaProjectFilter.INSTANCE & ~(new ProjectNameGlobFilter("*_test"))) // Loop over all non-test Java projects and create compile tasks for them javaProjects.each { proj -> // Create a temporary directory for compiled files def classDir = Directories.newDirectory(tmpDir, proj.name) classDirs << classDir // Create the compilation task def javacTask = new Jdk6JavacTF(). // A project may have several source directories addSources(proj.sourceDirectories). setTarget(classDir). // Add a JavaProjectClasspathDecorator that uses the // JavaProjectDependencies object to give the compile task its classpath addClasspathDecorator( new JavaProjectClasspathDecorator(). setProject(proj). setDependencies(projDeps)).create() // Register the compile task as the dependency that this project hinges on projDeps.registerDependency(proj, javacTask) // Also register the class directory so that it can be found by projects // that depend on this project projDeps.registerClassDirectory(proj, classDir) // Add the compile task to the task executor. Use the projDeps object to // get the dependencies te.add(javacTask, projDeps.getDependencies(proj)) } // Create a Jar file with all the compiled classes // (Since this task depends on all the compile tasks, it might as well have // been defined after the task executor has been shut down.) te.add( new JarTF(). // targetFile is a FutureFile setTarget(targetFile). addSources(classDirs), // The ProjectDependencies object is also a TaskDependency // object that is satisfied when all projects have been built. projDeps) // Wait for all tasks to complete te.waitFor() } finally { te.shutdown() }

JavaScript

// Create the Eclipse workspace object for the contents in the directory wos. // wos may be a File directory or an EntityFS Directory. // Create an EclipseWorkspaceSettings object that is used to override // information that is parsed from the workspace configuration files. settings = new EclipseWorkspaceSettings(); // Add a classpath variable to the settings object. // The EclipseWorkspace object tries to parse the values of classpath // variables from the workspace metadata. If they are not defined there, or if // a value there should be overridden, classpath variable values may be added // manually to the EclipseWorkspaceSettings object. settings.addClasspathVariable("MY_VARIABLE", new File("lib/mylib.jar")); eWos = new EclipseWorkspace(wos, settings); // To create an Eclipse workspace object for the contents in the Schmant // process' working directory, use // var eWos = new EclipseWorkspace(new File("."), settings); // Create a JavaProjectDependencies object for keeping track of dependencies // between different projects. projDeps = new JavaProjectDependencies(); // This collection will contain all the directories with class files classDirs = [] // The tmpDir variable points to a directory where temporary files are kept // during the build. It can for instance be created by calling // var tmpDir = TempFileUtil.createTempDirectory(); // Create a TaskExecutor and start it var te = new TaskExecutor(). setNumberOfThreads(2). start(); try { // Get all non-test Java projects from the workspace var javaProjects = eWos.getProjects( JavaProjectFilter.INSTANCE.and( new ProjectNameGlobFilter("*_test").not())); // Iterate over all non-test Java projects and create compile tasks for them itr = javaProjects.iterator(); while(itr.hasNext()) { proj = itr.next(); // Create a temporary directory for compiled files classDir = Directories.newDirectory(tmpDir, proj.getName()); classDirs.push(classDir); // Create the compilation task javacTask = new Jdk6JavacTF(). // A project may have several source directories addSources(proj.getSourceDirectories()). setTarget(classDir). // Add a JavaProjectClasspathDecorator that uses the // JavaProjectDependencies object to give the compile task its classpath addClasspathDecorator( new JavaProjectClasspathDecorator(). setProject(proj). setDependencies(projDeps)).create(); // Register the compile task as the dependency that this project hinges on projDeps.registerDependency(proj, javacTask); // Also register the class directory so that it can be found by projects // that depend on this project projDeps.registerClassDirectory(proj, classDir); // Add the compile task to the task executor. Use the projDeps object to // get the dependencies te.add(javacTask, projDeps.getDependencies(proj)); } // Create a Jar file with all the compiled classes // (Since this task depends on all the compile tasks, it might as well have // been defined after the task executor has been shut down.) te.add( new JarTF(). // targetFile is a FutureFile setTarget(targetFile). addSources(classDirs), // The ProjectDependencies object is also a TaskDependency // object that is satisfied when all projects have been built. projDeps); // Wait for all tasks to complete te.waitFor(); } finally { te.shutdown(); }

JRuby

# Create the Eclipse workspace object for the contents in the directory wos. # wos may be a File directory or an EntityFS Directory. # Create an EclipseWorkspaceSettings object that is used to override # information that is parsed from the workspace configuration files. settings = Schmant::EclipseWorkspaceSettings.new # Add a classpath variable to the settings object. # The EclipseWorkspace object tries to parse the values of classpath # variables from the workspace metadata. If they are not defined there, or if # a value there should be overridden, classpath variable values may be added # manually to the EclipseWorkspaceSettings object. settings.addClasspathVariable( "MY_VARIABLE", Java::JavaIo::File.new("lib/mylib.jar")) eWos = Schmant::EclipseWorkspace.new($wos, settings) # To create an Eclipse workspace object for the contents in the Schmant process' # working directory, use # eWos = Schmant::EclipseWorkspace.new(java.io.File.new("."), settings); # Create a JavaProjectDependencies object for keeping track of dependencies # between different projects. projDeps = Schmant::JavaProjectDependencies.new # This array will contain all the directories with class files classDirs = [] # The tmpDir variable points to a directory where temporary files are kept # during the build. It can for instance be created by calling # tmpDir = Schmant::TempFileUtil.createTempDirectory # Create a TaskExecutor and start it te = TaskExecutor.new. setNumberOfThreads(2). start begin # Get all non-test Java projects from the workspace javaProjects = eWos.getProjects( Schmant::JavaProjectFilter::INSTANCE.and( Schmant::ProjectNameGlobFilter.new("*_test").not)) # Loop over all non-test Java projects and create compile tasks for them javaProjects.each do |proj| # Create a temporary directory for compiled files classDir = Schmant::Directories.newDirectory($tmpDir, proj.name) classDirs << classDir # Create the compilation task javacTask = Schmant::Jdk6JavacTF.new. # A project may have several source directories addSources(proj.sourceDirectories). setTarget(classDir). # Add a JavaProjectClasspathDecorator that uses the # JavaProjectDependencies object to give the compile task its classpath addClasspathDecorator( Schmant::JavaProjectClasspathDecorator.new. setProject(proj). setDependencies(projDeps)).create # Register the compile task as the dependency that this project hinges on projDeps.registerDependency(proj, javacTask) # Also register the class directory so that it can be found by projects # that depend on this project projDeps.registerClassDirectory(proj, classDir) # Add the compile task to the task executor. Use the projDeps object to # get the dependencies te.add(javacTask, projDeps.getDependencies(proj)) end # Create a Jar file with all the compiled classes # (Since this task depends on all the compile tasks, it might as well have # been defined after the task executor has been shut down.) te.add( Schmant::JarTF.new. # targetFile is a FutureFile setTarget($targetFile). addSources(classDirs), # The ProjectDependencies object is also a TaskDependency # object that is satisfied when all projects have been built. projDeps) # Wait for all tasks to complete te.waitFor ensure te.shutdown end

Jython

# Create the Eclipse workspace object for the contents in the directory wos. # wos may be a File directory or an EntityFS Directory. # Create an EclipseWorkspaceSettings object that is used to override # information that is parsed from the workspace configuration files. settings = EclipseWorkspaceSettings() # Add a classpath variable to the settings object. # The EclipseWorkspace object tries to parse the values of classpath # variables from the workspace metadata. If they are not defined there, or if # a value there should be overridden, classpath variable values may be added # manually to the EclipseWorkspaceSettings object. settings.addClasspathVariable("MY_VARIABLE", File("lib/mylib.jar")) eWos = EclipseWorkspace(wos, settings) # To create an Eclipse workspace object for the contents in the Schmant process' # working directory, use # eWos = EclipseWorkspace(File("."), settings) # Create a JavaProjectDependencies object for keeping track of dependencies # between different projects. projDeps = JavaProjectDependencies() # This list will contain all the directories with class files classDirs = [] # The tmpDir variable points to a directory where temporary files are kept # during the build. It can for instance be created by calling # tmpDir = TempFileUtil.createTempDirectory() # Create a TaskExecutor and start it te = TaskExecutor(). \ setNumberOfThreads(2). \ start() try: # Get all non-test Java projects from the workspace javaProjects = \ eWos.getProjects( \ JavaProjectFilter.INSTANCE.and( \ ProjectNameGlobFilter("*_test").not())) # Iterate over all non-test Java projects and create compile tasks for them itr = javaProjects.iterator() while itr.hasNext(): proj = itr.next() # Create a temporary directory for compiled files classDir = Directories.newDirectory(tmpDir, proj.getName()) classDirs.append(classDir) # Create the compilation task # Add a JavaProjectClasspathDecorator that uses the # JavaProjectDependencies object to give the compile task its classpath javacTask = Jdk6JavacTF(). \ addSources(proj.getSourceDirectories()). \ setTarget(classDir). \ addClasspathDecorator( \ JavaProjectClasspathDecorator(). \ setProject(proj). \ setDependencies(projDeps)).create() # Register the compile task as the dependency that this project hinges on projDeps.registerDependency(proj, javacTask) # Also register the class directory so that it can be found by projects # that depend on this project projDeps.registerClassDirectory(proj, classDir) # Add the compile task to the task executor. Use the projDeps object to # get the dependencies te.add(javacTask, projDeps.getDependencies(proj)) # Create a Jar file with all the compiled classes # (Since this task depends on all the compile tasks, it might as well have # been defined after the task executor has been shut down.) # # targetFile is a FutureFile. # # The ProjectDependencies object is also a TaskDependency # object that is satisfied when all projects have been built. te.add( JarTF(). \ setTarget(targetFile). \ addSources(classDirs), \ projDeps) # Wait for all tasks to complete te.waitFor() finally: te.shutdown()

The next example shows how the user library EriksLib is defined and used when compiling a workspace.

Example 8.5. Build an Eclipse workspace using a user-defined library

Groovy

import org.entityfs.util.Directories import org.schmant.project.eclipse.* import org.schmant.project.java.Library import org.schmant.task.project.JavaWorkspaceBuilderTF // Create an EclipseWorkspaceSettings object that is used to provide information // that cannot be parsed from its .project and .classpath files to the Eclipse // workspace object. def settings = new EclipseWorkspaceSettings() // The EriksLib library contains all Jar files in the lib directory. def eriksLib = new Library(). addEntries(Directories.getAllFilesMatching(lib, "*.jar")) // Add the user library to the settings object. The name used for the library is // the name used in the Eclipse project's .classpath file. settings.addLibrary("org.eclipse.jdt.USER_LIBRARY/EriksLib", eriksLib) // Create an EclipseWorkspace object for the workspace in the directory wos. // wos may be a File directory or an EntityFS Directory. def eWos = new EclipseWorkspace(wos, settings) // Build it new JavaWorkspaceBuilderTF(). setWorkspace(eWos). // This is a File directory or a Directory setTarget(bin). run()

JavaScript

// Create an EclipseWorkspaceSettings object that is used to provide information // that cannot be parsed from its .project and .classpath files to the Eclipse // workspace object. settings = new EclipseWorkspaceSettings(); // The EriksLib library contains all Jar files in the lib directory. eriksLib = new Library(). addEntries(Directories.getAllFilesMatching(lib, "*.jar")); // Add the user library to the settings object. The name used for the library is // the name used in the Eclipse project's .classpath file. settings.addLibrary("org.eclipse.jdt.USER_LIBRARY/EriksLib", eriksLib); // Create an EclipseWorkspace object for the workspace in the directory wos. // wos may be a File directory or an EntityFS Directory. eWos = new EclipseWorkspace(wos, settings); // Build it new JavaWorkspaceBuilderTF(). setWorkspace(eWos). // This is a File directory or a Directory setTarget(bin). run();

JRuby

# Create an EclipseWorkspaceSettings object that is used to provide information # that cannot be parsed from its .project and .classpath files to the Eclipse # workspace object. settings = Schmant::EclipseWorkspaceSettings.new # The EriksLib library contains all Jar files in the lib directory. eriksLib = Schmant::Library.new. addEntries(Schmant::Directories.getAllFilesMatching($lib, "*.jar")) # Add the user library to the settings object. The name used for the library is # the name used in the Eclipse project's .classpath file. settings.addLibrary("org.eclipse.jdt.USER_LIBRARY/EriksLib", eriksLib) # Create an EclipseWorkspace object for the workspace in the directory wos. # wos may be a File directory or an EntityFS Directory. eWos = EclipseWorkspace.new($wos, settings) # Build it Schmant::JavaWorkspaceBuilderTF.new. setWorkspace(eWos). # This is a File directory or a Directory setTarget($bin). run

Jython

# Create an EclipseWorkspaceSettings object that is used to provide information # that cannot be parsed from its .project and .classpath files to the Eclipse # workspace object. settings = EclipseWorkspaceSettings() # The EriksLib library contains all Jar files in the lib directory. eriksLib = Library(). \ addEntries(Directories.getAllFilesMatching(lib, "*.jar")) # Add the user library to the settings object. The name used for the library is # the name used in the Eclipse project's .classpath file. settings.addLibrary("org.eclipse.jdt.USER_LIBRARY/EriksLib", eriksLib) # Create an EclipseWorkspace object for the workspace in the directory wos. # wos may be a File directory or an EntityFS Directory. eWos = EclipseWorkspace(wos, settings) # Build it # bin is a File directory or a Directory JavaWorkspaceBuilderTF(). \ setWorkspace(eWos). \ setTarget(bin). \ run()

In the next two examples, all source files are preprocessed before they are compiled.

Example 8.6. Preprocess source files in Eclipse projects. Compile. Build Jar

Groovy

import org.entityfs.util.filter.entity.* import org.schmant.project.eclipse.EclipseWorkspace import org.schmant.support.io.TempFileUtil import org.schmant.task.jdk.jar.JarTF import org.schmant.task.meta.RecursiveProcessTF import org.schmant.task.project.JavaWorkspaceBuilderTF import org.schmant.task.text.TextReplaceTF // Create the Eclipse workspace object for the contents in the directory wos. // wos may be a File directory or an EntityFS Directory. def eWos = new EclipseWorkspace(wos) // An EntityFilter that hides Subversion .svn directories def noSvnFilter = ~(DirectoryFilter.FILTER & new EntityNameFilter(".svn")) // The tmpDir variable points to a directory where temporary files are kept // during the build. It can for instance be created by calling // tmpDir = TempFileUtil.createTempDirectory() new JavaWorkspaceBuilderTF(). setWorkspace(eWos). setTarget(tmpDir). // // Set a task factory for creating preprocess tasks. A task will be created // from this factory for each source directory for each project. The source // and target properties are set automatically. setPreprocessTaskFactory( // // Recurse through the source directory. new RecursiveProcessTF(). setTaskFactory( new TextReplaceTF(). addReplace("!!!VERSION!!!", "1.0"). // (For the documentation's unit tests. Pretend it's not here...) addReplace("232", "233"))). run() // Create a Jar file with all the compiled classes new JarTF(). // targetFile is a File or a FutureFile setTarget(targetFile). setSource(tmpDir). run()

JavaScript

// Create the Eclipse workspace object for the contents in the directory wos. // wos may be a File directory or an EntityFS Directory. eWos = new EclipseWorkspace(wos); // An EntityFilter that hides Subversion .svn directories noSvnFilter = DirectoryFilter.FILTER.and( new EntityNameFilter(".svn")).not(); // The tmpDir variable points to a directory where temporary files are kept // during the build. It can for instance be created by calling // tmpDir = TempFileUtil.createTempDirectory(); new JavaWorkspaceBuilderTF(). setWorkspace(eWos). setTarget(tmpDir). // // Set a task factory for creating preprocess tasks. A task will be created // from this factory for each source directory for each project. The source // and target properties are set automatically. setPreprocessTaskFactory( // // Recurse through the source directory. new RecursiveProcessTF(). setTaskFactory( new TextReplaceTF(). addReplace("!!!VERSION!!!", "1.0"). // (For the documentation's unit tests. Pretend it's not here...) addReplace("232", "233"))). run(); // Create a Jar file with all the compiled classes new JarTF(). // targetFile is a File or a FutureFile setTarget(targetFile). setSource(tmpDir). run();

JRuby

# Create the Eclipse workspace object for the contents in the directory wos. # wos may be a File directory or an EntityFS Directory. eWos = Schmant::EclipseWorkspace.new $wos # An EntityFilter that hides Subversion .svn directories noSvnFilter = Schmant::DirectoryFilter::FILTER.and( Schmant::EntityNameFilter.new(".svn")).not # The $tmpDir variable points to a directory where temporary files are kept # during the build. It can for instance be created by calling # tmpDir = TempFileUtil.createTempDirectory Schmant::JavaWorkspaceBuilderTF.new. setWorkspace(eWos). setTarget($tmpDir). # # Set a task factory for creating preprocess tasks. A task will be created # from this factory for each source directory for each project. The source # and target properties are set automatically. setPreprocessTaskFactory( # # Recurse through the source directory. Schmant::RecursiveProcessTF.new. setTaskFactory( Schmant::TextReplaceTF.new. addReplace("!!!VERSION!!!", "1.0"). # (For the documentation's unit tests. Pretend it's not here...) addReplace("232", "233"))). run # Create a Jar file with all the compiled classes Schmant::JarTF.new. # targetFile is a File or a FutureFile setTarget($targetFile). setSource($tmpDir).run

Jython

# Create the Eclipse workspace object for the contents in the directory wos. # wos may be a File directory or an EntityFS Directory. eWos = EclipseWorkspace(wos) # An EntityFilter that hides Subversion .svn directories noSvnFilter = DirectoryFilter.FILTER.and( EntityNameFilter(".svn")).not() # The tmpDir variable points to a directory where temporary files are kept # during the build. It can for instance be created by calling # tmpDir = TempFileUtil.createTempDirectory() # Build the workspace # # Set a task factory for creating preprocess tasks. A task will be created from # this factory for each source directory for each project. The source and target # target properties are set automatically. # # Recurse through the source directory. # # The replace 232 -> 233 is for the documentation's unit tests. Pretend it's not # there... JavaWorkspaceBuilderTF(). \ setWorkspace(eWos). \ setTarget(tmpDir). \ setPreprocessTaskFactory( RecursiveProcessTF(). \ setTaskFactory( TextReplaceTF(). \ addReplace("!!!VERSION!!!", "1.0"). \ addReplace("232", "233"))). \ run() # Create a Jar file with all the compiled classes # # targetFile is a File or a FutureFile JarTF(). \ setTarget(targetFile). \ setSource(tmpDir). \ run()

Example 8.7. Preprocess source files in Eclipse projects. Compile manually. Build Jar

Groovy

import org.entityfs.util.Directories import org.entityfs.util.filter.entity.* import org.schmant.arg.DirectoryAndFilter import org.schmant.project.eclipse.* import org.schmant.project.java.* import org.schmant.run.* import org.schmant.support.io.TempFileUtil import org.schmant.task.jdk.jar.JarTF import org.schmant.task.jdk.javac.jdk6.Jdk6JavacTF import org.schmant.task.meta.RecursiveProcessTF import org.schmant.task.text.TextReplaceTF // Create the Eclipse workspace object for the contents in the directory wos. // wos may be a File directory or an EntityFS Directory. // vars is a Map of the values of classpath variables used in // the workspace. These variables override the values parsed from the workspace // metadata. def settings = new EclipseWorkspaceSettings(). addClasspathVariables(vars) def eWos = new EclipseWorkspace(wos, settings) // Get all Java projects from the workspace def javaProjects = eWos.getProjects().findAll{ proj -> JavaProjectFilter.INSTANCE.matches(proj)} // An EntityFilter that hides Subversion .svn directories def noSvnFilter = ~(DirectoryFilter.FILTER & new EntityNameFilter(".svn")) // Create a JavaProjectDependencies object for keeping track of dependencies // between different projects. def projDeps = new JavaProjectDependencies() // This collection will contain all the directories with class files def classDirs = [] // Create a TaskExecutor and start it def te = new TaskExecutor(). setNumberOfThreads(props.getIntValue("noOfBuildThreads", 2)). start() try { // The tmpDir variable points to a directory where temporary files are kept // during the build. It can for instance be created by calling // def tmpDir = TempFileUtil.createTempDirectory() // Create compile tasks for each Java project javaProjects.each{ proj -> def projName = proj.name // Create a temporary directory to put the preprocessed source files in def sourceDir = Directories.newDirectory(tmpDir, projName + "_src") // Create the preprocess task for all source files def ppt = new RecursiveProcessTF(). // Preprocess all files in the directory hierarchy. Ignore Subversion's // .svn directories. addSources( DirectoryAndFilter.listWithFilter( Directories.newViews(proj.sourceDirectories, noSvnFilter), EFileFilter.FILTER)). setTarget(sourceDir). setTaskFactory( new TextReplaceTF(). addReplace("!!!VERSION!!!", "1.0"). // (For the documentation's unit tests. Pretend it's not here...) addReplace("232", "233")). create() te.add(ppt) // Create a temporary directory for compiled files def classDir = Directories.newDirectory(tmpDir, projName) classDirs.add(classDir) // Create the compilation task def javacTask = new Jdk6JavacTF(). addSource(sourceDir). setTarget(classDir). // Add a JavaProjectClasspathDecorator that uses the // JavaProjectDependencies object created above to give the compile // task its classpath addClasspathDecorator( new JavaProjectClasspathDecorator(). setProject(proj). setDependencies(projDeps)).create() // Register the compile task as the dependency that this project hinges on. // (If we would post process the compiled class files in any way, for // instance Emma instrument them, the post process task would (probably) be // the one that was registered here.) projDeps.registerDependency(proj, javacTask) // Also register the class directory so that it can be found by projects // that depend on this project. projDeps.registerClassDirectory(proj, classDir) // Add the compile task to the task executor. Now the compile task hinges // on both it's dependent projects and the preprocess task te.add( javacTask, new CompoundTaskDependency(). addAll(projDeps.getDependencies(proj)). add(ppt)) } // Create a Jar file with all the compiled classes te.add( new JarTF(). // targetFile is a File or a FutureFile setTarget(targetFile). addSources(classDirs), // This is also a dependency object for all compilation tasks. projDeps) // Wait for all tasks to complete te.waitFor() } finally { te.shutdown() }

JavaScript

// Create the Eclipse workspace object for the contents in the directory wos. // wos may be a File directory or an EntityFS Directory. // vars is a Map of the values of classpath variables used in // the workspace. These variables override the values parsed from the workspace // metadata. settings = new EclipseWorkspaceSettings(). addClasspathVariables(vars); eWos = new EclipseWorkspace(wos, settings); // Get all Java projects from the workspace javaProjects = eWos.getProjects( JavaProjectFilter.INSTANCE); // An EntityFilter that hides Subversion .svn directories noSvnFilter = DirectoryFilter.FILTER.and( new EntityNameFilter(".svn")).not(); // Create a JavaProjectDependencies object for keeping track of dependencies // between different projects. projDeps = new JavaProjectDependencies(); // This collection will contain all the directories with class files classDirs = new ArrayList(); // Create a TaskExecutor and start it te = new TaskExecutor(). setNumberOfThreads(props.getIntValue("noOfBuildThreads", 2)). start(); try { // The tmpDir variable points to a directory where temporary files are kept // during the build. It can for instance be created by calling // var tmpDir = TempFileUtil.createTempDirectory(); // Iterate over all Java projects and create compile tasks for them itr = javaProjects.iterator(); while(itr.hasNext()) { proj = itr.next(); projName = proj.getName(); // Create a temporary directory to put the preprocessed source files in sourceDir = Directories.newDirectory(tmpDir, projName + "_src"); // Create the preprocess task for all source files ppt = new RecursiveProcessTF(). // Preprocess all files in the directory hierarchy. Ignore Subversion's // .svn directories. addSources( DirectoryAndFilter.listWithFilter( Directories.newViews(proj.getSourceDirectories(), noSvnFilter), EFileFilter.FILTER)). setTarget(sourceDir). setTaskFactory( new TextReplaceTF(). addReplace("!!!VERSION!!!", "1.0"). // (For the documentation's unit tests. Pretend it's not here...) addReplace("232", "233")). create(); te.add(ppt); // Create a temporary directory for compiled files classDir = Directories.newDirectory(tmpDir, projName); classDirs.add(classDir); // Create the compilation task javacTask = new Jdk6JavacTF(). addSource(sourceDir). setTarget(classDir). // Add a JavaProjectClasspathDecorator that uses the // JavaProjectDependencies object created above to give the compile // task its classpath addClasspathDecorator( new JavaProjectClasspathDecorator(). setProject(proj). setDependencies(projDeps)).create(); // Register the compile task as the dependency that this project hinges on. // (If we would post process the compiled class files in any way, for // instance Emma instrument them, the post process task would (probably) be // the one that was registered here.) projDeps.registerDependency(proj, javacTask); // Also register the class directory so that it can be found by projects // that depend on this project. projDeps.registerClassDirectory(proj, classDir); // Add the compile task to the task executor. Now the compile task hinges // on both it's dependent projects and the preprocess task te.add( javacTask, new CompoundTaskDependency(). addAll(projDeps.getDependencies(proj)). add(ppt)); } // Create a Jar file with all the compiled classes te.add( new JarTF(). // targetFile is a File or a FutureFile setTarget(targetFile). addSources(classDirs), // This is also a dependency object for all compilation tasks. projDeps); // Wait for all tasks to complete te.waitFor(); } finally { te.shutdown(); }

JRuby

# Create the Eclipse workspace object for the contents in the directory wos. # wos may be a File directory or an EntityFS Directory. # vars is a Map of the values of classpath variables used in # the workspace. These variables override the values parsed from the workspace # metadata. settings = Schmant::EclipseWorkspaceSettings.new. addClasspathVariables($vars) eWos = Schmant::EclipseWorkspace.new($wos, settings) # Get all Java projects from the workspace javaProjects = eWos.getProjects Schmant::JavaProjectFilter::INSTANCE # An EntityFilter that hides Subversion .svn directories noSvnFilter = Schmant::DirectoryFilter::FILTER.and( Schmant::EntityNameFilter.new(".svn")).not # Create a JavaProjectDependencies object for keeping track of dependencies # between different projects. projDeps = Schmant::JavaProjectDependencies.new # This list will contain all the directories with class files classDirs = [] # Create a TaskExecutor and start it te = Schmant::TaskExecutor.new. setNumberOfThreads($props.getIntValue("noOfBuildThreads", 2)). start begin # The tmpDir variable points to a directory where temporary files are kept # during the build. It can for instance be created by calling # tmpDir = TempFileUtil.createTempDirectory # Iterate over all Java projects and create compile tasks for them javaProjects.each do |proj| projName = proj.name # Create a temporary directory to put the preprocessed source files in sourceDir = Schmant::Directories.newDirectory($tmpDir, projName + "_src") # Create the preprocess task for all source files ppt = Schmant::RecursiveProcessTF.new. # Preprocess all files in the directory hierarchy. Ignore Subversion's # .svn directories. addSources( Schmant::DirectoryAndFilter.listWithFilter( Schmant::Directories.newViews(proj.sourceDirectories, noSvnFilter), Schmant::EFileFilter::FILTER)). setTarget(sourceDir). setTaskFactory( Schmant::TextReplaceTF.new. addReplace("!!!VERSION!!!", "1.0"). # (For the documentation's unit tests. Pretend it's not here...) addReplace("232", "233")).create te.add ppt # Create a temporary directory for compiled files classDir = Schmant::Directories.newDirectory($tmpDir, projName) classDirs.push classDir # Create the compilation task javacTask = Schmant::Jdk6JavacTF.new. addSource(sourceDir). setTarget(classDir). # Add a JavaProjectClasspathDecorator that uses the # JavaProjectDependencies object created above to give the compile # task its classpath addClasspathDecorator( Schmant::JavaProjectClasspathDecorator.new. setProject(proj). setDependencies(projDeps)).create # Register the compile task as the dependency that this project hinges on. # (If we would post process the compiled class files in any way, for # instance Emma instrument them, the post process task would (probably) be # the one that was registered here.) projDeps.registerDependency(proj, javacTask) # Also register the class directory so that it can be found by projects # that depend on this project. projDeps.registerClassDirectory(proj, classDir) # Add the compile task to the task executor. Now the compile task hinges # on both it's dependent projects and the preprocess task te.add( javacTask, Schmant::CompoundTaskDependency.new. addAll(projDeps.getDependencies(proj)). add(ppt)) end # Create a Jar file with all the compiled classes te.add( Schmant::JarTF.new. # targetFile is a File or a FutureFile setTarget($targetFile). addSources(classDirs), # This is also a dependency object for all compilation tasks. projDeps) # Wait for all tasks to complete te.waitFor ensure te.shutdown end

Jython

# Create the Eclipse workspace object for the contents in the directory wos. # wos may be a File directory or an EntityFS Directory. # vars is a Map of the values of classpath variables used in # the workspace. These variables override the values parsed from the workspace # metadata. settings = EclipseWorkspaceSettings(). \ addClasspathVariables(vars) eWos = EclipseWorkspace(wos, settings) # Get all Java projects from the workspace javaProjects = eWos.getProjects( JavaProjectFilter.INSTANCE) # An EntityFilter that hides Subversion .svn directories noSvnFilter = DirectoryFilter.FILTER.and( EntityNameFilter(".svn")).not() # Create a JavaProjectDependencies object for keeping track of dependencies # between different projects. projDeps = JavaProjectDependencies() # This collection will contain all the directories with class files classDirs = [] # Create a TaskExecutor and start it te = TaskExecutor(). \ setNumberOfThreads(props.getIntValue("noOfBuildThreads", 2)). \ start() try: # The tmpDir variable points to a directory where temporary files are # kept during the build. It can for instance be created by calling # tmpDir = TempFileUtil.createTempDirectory() # Iterate over all Java projects and create compile tasks for them itr = javaProjects.iterator() while(itr.hasNext()): proj = itr.next() projName = proj.getName() # Create a temporary directory to put the preprocessed source # files in. sourceDir = Directories.newDirectory(tmpDir, projName + "_src") # Create the preprocess task for all source files # # Preprocess all files in the directory hierarchy. Ignore # Subversion's .svn directories. # # The replace 232 -> 233 is for the documentation's unit tests. # Pretend that it's not there... ppt = RecursiveProcessTF(). \ addSources( DirectoryAndFilter.listWithFilter( Directories.newViews( proj.getSourceDirectories(), \ noSvnFilter), \ EFileFilter.FILTER)). \ setTarget(sourceDir). \ setTaskFactory( TextReplaceTF(). \ addReplace("!!!VERSION!!!", "1.0"). \ addReplace("232", "233")). \ create() te.add(ppt) # Create a temporary directory for compiled files classDir = Directories.newDirectory(tmpDir, projName) classDirs.append(classDir) # Create the compilation task # # Add a JavaProjectClasspathDecorator that uses # the JavaProjectDependencies object created above to give the # compile task its classpath javacTask = Jdk6JavacTF(). \ addSource(sourceDir). \ setTarget(classDir). \ addClasspathDecorator( JavaProjectClasspathDecorator(). \ setProject(proj). \ setDependencies(projDeps)).create() # Register the compile task as the dependency that this project # hinges on. (If we would post process the compiled class files # in any way, for instance Emma instrument them, the post # process task would (probably) be the one that was registered # here.) projDeps.registerDependency(proj, javacTask) # Also register the class directory so that it can be found by # projects that depend on this project. projDeps.registerClassDirectory(proj, classDir) # Add the compile task to the task executor. Now the compile # task hinges on both it's dependent projects and the preprocess # task. te.add( javacTask, \ CompoundTaskDependency(). \ addAll(projDeps.getDependencies(proj)). \ add(ppt)) # End while # Create a Jar file with all the compiled classes # # targetFile is a File or a FutureFile # # projDeps is also a dependency object for all compilation tasks. te.add( JarTF(). \ setTarget(targetFile). \ addSources(classDirs), \ projDeps) # Wait for all tasks to complete te.waitFor() finally: te.shutdown()

The EclipseWorkspace uses a plugin mechanism for parsing project directories. Task packages may register new plugins that teaches it to create new kinds of Project objects. See the reference documentation for details.

IntelliJ IDEA project support is provided by the IntelliJWorkspace class.

Note

There is a bit of name confusion at work here. The equivalent of the Eclipse workspace is called a project in IntelliJ and the equivalent of the Eclipse project is called a module. But, to try to keep some semblance of consistency, in Schmant and from now on in this manual, an IntelliJ project is called a workspace and an IntelliJ module is called a project. Sorry for the confusion.

The IntelliJWorkspace supports several types of project dependencies. A project can have dependencies to module libraries, project libraries, global libraries and application-supplied (IntelliJ-supplied) libraries such as the Java EE libraries, as well as to single Jar files and class file directories. If used by the workspace, information about global and application-supplied libraries has to be provided in an IntelliJWorkspaceSettings object when creating the workspace object. All other necessary information about where to locate dependencies is parsed from the workspace's .ipr and .iml files by the IntelliJWorkspace object.

An IntelliJ JavaProject also implements the IntelliJJavaProject interface which adds a getTestSourceDirectories method for getting a project's test sources.

The example below shows how to build an IntelliJ workspace with global and application-supplied library dependencies.

Example 8.8. Build an IntelliJ workspace

Groovy

import java.io.File import org.entityfs.el.AbsoluteLocation import org.entityfs.util.* import org.schmant.project.intellij.* import org.schmant.project.java.Library import org.schmant.task.project.JavaWorkspaceBuilderTF // Create an IntelliJWorkspaceSettings object that is used to provide // information that cannot be parsed from its .ipr and .iml files to the // IntelliJ workspace object. def settings = new IntelliJWorkspaceSettings(). // Supply the IntelliJ IDEA installation directory. You don't need to do this // if your workspace does not use any application-supplied libraries. setApplicationHomeDirectory( FileSystems.getEntityForDirectory( new File("/home/kalle/java/idea-7590"), true)); // The GlobalLib library uses all Jar files in the lib and lib2 directories and // the class files in the directory tree under the classes directory. def globalLib = new Library(). addEntries(Directories.getAllFilesMatching(lib, "*.jar")). addEntries(Directories.getAllFilesMatching(lib2, "*.jar")). addEntry(classes) // Add the global library to the settings. settings.addGlobalLibrary("GlobLib1", globalLib) // Create an IntelliJWorkspace object for the workspace in the directory wos. // Supply both the global library and the path to the IntelliJ installation. // // If you don't want to use a proper IntelliJ installation for the application- // supplied libraries, use any old directory that contains all required library // files in the places where the workspace expects them. If a required file // cannot be found, the workspace object throws an exception with a message that // can be used to fix the problem. // // The application installation path or the global library map can be set to // null if they are not used by the workspace. def ijWos = new IntelliJWorkspace( wos, // We have to tell the constructor where to find the workspace's .ipr file new AbsoluteLocation("/MyProject.ipr"), settings) // Build the workspace. // The default is to build all Java files in the source and the test source // directories. The dontBuildTestSources and dontBuildRegularSources properties // may be set to avoid building either kind of sources. new JavaWorkspaceBuilderTF(). setWorkspace(ijWos). // bin is a Java File or an EntityFS Directory. Class files built // from both the regular and the test sources are put here. setTarget(bin). run()

JavaScript

// Create an IntelliJWorkspaceSettings object that is used to provide // information that cannot be parsed from its .ipr and .iml files to the // IntelliJ workspace object. settings = new IntelliJWorkspaceSettings(). // Supply the IntelliJ IDEA installation directory. You don't need to do this // if your workspace does not use any application-supplied libraries. setApplicationHomeDirectory( FileSystems.getEntityForDirectory( new File("/home/kalle/java/idea-7590"), true)); // The GlobalLib library uses all Jar files in the lib and lib2 directories and // the class files in the directory tree under the classes directory. globalLib = new Library(). addEntries(Directories.getAllFilesMatching(lib, "*.jar")). addEntries(Directories.getAllFilesMatching(lib2, "*.jar")). addEntry(classes); // Add the global library to the settings. settings.addGlobalLibrary("GlobLib1", globalLib); // Create an IntelliJWorkspace object for the workspace in the directory wos. // Supply both the global library and the path to the IntelliJ installation. // // If you don't want to use a proper IntelliJ installation for the application- // supplied libraries, use any old directory that contains all required library // files in the places where the workspace expects them. If a required file // cannot be found, the workspace object throws an exception with a message that // can be used to fix the problem. // // The application installation path or the global library map can be set to // null if they are not used by the workspace. ijWos = new IntelliJWorkspace( wos, // We have to tell the constructor where to find the workspace's .ipr file new AbsoluteLocation("/MyProject.ipr"), settings); // Build the workspace. // The default is to build all Java files in the source and the test source // directories. The dontBuildTestSources and dontBuildRegularSources properties // may be set to avoid building either kind of sources. new JavaWorkspaceBuilderTF(). setWorkspace(ijWos). // bin is a Java File or an EntityFS Directory. Class files built // from both the regular and the test sources are put here. setTarget(bin). run();

JRuby

# Create an IntelliJWorkspaceSettings object that is used to provide # information that cannot be parsed from its .ipr and .iml files to the # IntelliJ workspace object. settings = Schmant::IntelliJWorkspaceSettings.new. # Supply the IntelliJ IDEA installation directory. You don't need to do this # if your workspace does not use any application-supplied libraries. setApplicationHomeDirectory( Schmant::FileSystems.getEntityForDirectory( Java::JavaIo::File.new("/home/kalle/java/idea-7590"), true)) # The GlobalLib library uses all Jar files in the lib and lib2 directories and # the class files in the directory tree under the classes directory. globalLib = Schmant::Library.new. addEntries(Schmant::Directories.getAllFilesMatching($lib, "*.jar")). addEntries(Schmant::Directories.getAllFilesMatching($lib2, "*.jar")). addEntry($classes) # Add the global library to the settings. settings.addGlobalLibrary("GlobLib1", globalLib) # Create an IntelliJWorkspace object for the workspace in the directory wos. # Supply both the global library and the path to the IntelliJ installation. # # If you don't want to use a proper IntelliJ installation for the application- # supplied libraries, use any old directory that contains all required library # files in the places where the workspace expects them. If a required file # cannot be found, the workspace object throws an exception with a message that # can be used to fix the problem. # # The application installation path or the global library map can be set to # null if they are not used by the workspace. ijWos = Schmant::IntelliJWorkspace.new( $wos, # We have to tell the constructor where to find the workspace's .ipr file Schmant::AbsoluteLocation.new("/MyProject.ipr"), settings) # Build the workspace. # The default is to build all Java files in the source and the test source # directories. The dontBuildTestSources and dontBuildRegularSources properties # may be set to avoid building either kind of sources. Schmant::JavaWorkspaceBuilderTF.new. setWorkspace(ijWos). # bin is a Java File or an EntityFS Directory. Class files built # from both the regular and the test sources are put here. setTarget($bin). run

Jython

# Create an IntelliJWorkspaceSettings object that is used to provide # information that cannot be parsed from its .ipr and .iml files to the # IntelliJ workspace object. # # The IntelliJ IDEA installation directory has to be supplied if the workspace # does not use any application-supplied libraries. settings = IntelliJWorkspaceSettings(). \ setApplicationHomeDirectory( \ FileSystems.getEntityForDirectory( \ File("/home/kalle/java/idea-7590"), \ True)) # The GlobalLib library uses all Jar files in the lib and lib2 directories and # the class files in the directory tree under the classes directory. globalLib = Library(). \ addEntries(Directories.getAllFilesMatching(lib, "*.jar")). \ addEntries(Directories.getAllFilesMatching(lib2, "*.jar")). \ addEntry(classes) # Add the global library to the settings. settings.addGlobalLibrary("GlobLib1", globalLib) # Create an IntelliJWorkspace object for the workspace in the directory wos. # Supply both the global library and the path to the IntelliJ installation. # # If you don't want to use a proper IntelliJ installation for the application- # supplied libraries, use any old directory that contains all required library # files in the places where the workspace expects them. If a required file # cannot be found, the workspace object throws an exception with a message that # can be used to fix the problem. # # The application installation path or the global library map can be set to # null if they are not used by the workspace. ijWos = IntelliJWorkspace( \ wos, \ AbsoluteLocation("/MyProject.ipr"), \ settings) # Build the workspace. # The default is to build all Java files in the source and the test source # directories. The dontBuildTestSources and dontBuildRegularSources properties # may be set to avoid building either kind of sources. # # bin is a Java File or an EntityFS Directory. Class files built from # both the regular and the test sources are put here. JavaWorkspaceBuilderTF(). \ setWorkspace(ijWos). \ setTarget(bin). \ run()

The IntelliJWorkspace uses a plugin mechanism for parsing project directories. Task packages may register new plugins that teaches it to create new kinds of Project objects. See the reference documentation for details.

EntityFS-enabled tasks can use any type of file system implementations. One option is the in-memory file system created by the RamFileSystemBuilder object or by the SchmantFileSystems.createRamFileSystem() method. Keeping temporary files in memory can be used to speed up builds somewhat. This is certainly suggested by the EntityFS performance tests. The drawback is that debugging may be harder since the -k flag (obviously) does not make Schmant keep files stored in memory once the build process has terminated.

Consider the example Example 8.4, “Build a Jar file from the projects in an Eclipse workspace manually” from Chapter 8, Projects. If the tmpDir variable is created by calling TempFileUtil.createTempDir(), as suggested in the script, tmpDir will be on disk. If it is created as shown in Example 9.1, “Creating an in-memory temporary directory” it will be in RAM memory instead.

Example 9.1. Creating an in-memory temporary directory

Groovy

import org.entityfs.ram.RamFileSystemBuilder import org.entityfs.util.Directories import org.schmant.report.SchmantReportLogAdapter def fs = new RamFileSystemBuilder(). // The default size of a file segment is 4096 bytes. (A file consists of as // many segments as is needed for containing all its data.) // setFileSegmentSizeBytes(8192). // // Integrate the file system's logging with Schmant's reports setLogAdapter(SchmantReportLogAdapter.INSTANCE). create() def root = fs.rootDirectory // This would do exactly the same: // def root = SchmantFileSystems.createRamFileSystem() // Set a default temporary files directory on the file system. If not set, // temporary files end up in java.io.tmpdir instead. fs.setTemporaryFilesDirectory( Directories.newDirectory(root, "tmp2")) // And our temporary files directory tmpDir = Directories.newDirectory(root, "tmp")

JavaScript

fs = new RamFileSystemBuilder(). // The default size of a file segment is 4096 bytes. (A file consists of as // many segments as is needed for containing all its data.) // setFileSegmentSizeBytes(8192). // // Integrate the file system's logging with Schmant's reports setLogAdapter(SchmantReportLogAdapter.INSTANCE). create(); root = fs.getRootDirectory(); // This would do exactly the same: // var root = SchmantFileSystems.createRamFileSystem(); // Set a default temporary files directory on the file system. If not set, // temporary files end up in java.io.tmpdir instead. fs.setTemporaryFilesDirectory( Directories.newDirectory(root, "tmp2")); // And our temporary files directory tmpDir = Directories.newDirectory(root, "tmp");

JRuby

# The org.entityfs.ram package is not included in the Schmant module. fs = Java::OrgEntityfsRam::RamFileSystemBuilder.new. # The default size of a file segment is 4096 bytes. (A file consists of as # many segments as is needed for containing all its data.) # setFileSegmentSizeBytes(8192). # # Integrate the file system's logging with Schmant's reports setLogAdapter(Schmant::SchmantReportLogAdapter::INSTANCE). create root = fs.rootDirectory # This would do exactly the same: # root = Schmant::SchmantFileSystems.createRamFileSystem # Set a default temporary files directory on the file system. If not set, # temporary files end up in java.io.tmpdir instead. fs.setTemporaryFilesDirectory( Schmant::Directories.newDirectory(root, "tmp2")) # And our temporary files directory tmpDir = Schmant::Directories.newDirectory(root, "tmp")

Jython

# The org.entityfs.ram classes are not imported by default from org.entityfs.ram import RamFileSystemBuilder # The default size of a file segment is 4096 bytes. (A file consists of as many # segments as is needed for containing all its data.) # # Integrate the file system's logging with Schmant's reports fs = RamFileSystemBuilder(). \ setLogAdapter(SchmantReportLogAdapter.INSTANCE). \ create() root = fs.getRootDirectory() # This would do exactly the same: # root = SchmantFileSystems.createRamFileSystem() # Set a default temporary files directory on the file system. If not set, # temporary files end up in java.io.tmpdir instead. fs.setTemporaryFilesDirectory( \ Directories.newDirectory(root, "tmp2")) # And our temporary files directory tmpDir = Directories.newDirectory(root, "tmp")

The following table shows the execution times when building the Schmant workspace using Sun JDK 1.6.0_12 on a quad-core, Ubuntu 8.10 machine. Every time is taken as the average build time of five builds.


When running the build script from Example 8.7, “Preprocess source files in Eclipse projects. Compile manually. Build Jar” instead, on the same machine, the build times are as in the table below.


In the second example, the speedup from using temporary files is somewhat larger. This is probably because the TextReplaceTF tasks are more I/O bound than the Jdk6JavacTF tasks.

The conclusion is that using a RAM directory for temporary files can give a moderate speed boost for I/O intensive builds. The tradeoff is a higher memory usage, and that scripts may be harder to troubleshoot since temporary files cannot be saved for analyzing after the script has been run.

Note

The memory-backed directory /dev/shm can be used to speed up builds on Linux.

This chapter contains some best practices, based on experiences from real-world projects where Schmant is used.

Use EntityFS file systems to store build files in. Then the build script and the tasks it uses can use EntityFS utility methods for working with files and directories.

If building from a local workspace, use a read only file system for the source files and a read/write file system for storing build artifacts and temporary build files.

Example 10.1. Creating file systems for the build script

Groovy

import java.io.File import org.entityfs.util.Directories import org.entityfs.util.filter.entity.EntityNameFilter import org.schmant.support.entityfs.SchmantFileSystems import org.schmant.support.io.TempFileUtil // Create a read only file system with its root directory in // /home/me/myproject/src srcd = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/src"), true) // Get a view of the root directory that hides all .svn directories. When this // view is used for getting child directories, all .svn directories will be // hidden in them too, making all .svn directories in the file system invisible // for all EntityFS-aware tasks. // The ~ is used to negate the filter. srcRoot = srcd.newView(~(new EntityNameFilter(".svn"))) // Create a read/write file system in a temporary directory to use for putting // built files in. Keep this directory when the script is done (set keep flag to // true). targetRoot = TempFileUtil.createTempDirectory( "myproject", null, true) // Set a temporary directory on the target file system. Tasks that need to // create temporary files use this directory for them. targetRoot.fileSystem.setTemporaryFilesDirectory( Directories.newDirectory(targetRoot, "tmp"))

JavaScript

// Create a read only file system with its root directory in // /home/me/myproject/src srcd = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/src"), true); // Get a view of the root directory that hides all .svn directories. When this // view is used for getting child directories, all .svn directories will be // hidden in them too, making all .svn directories in the file system invisible // for all EntityFS-aware tasks. srcRoot = srcd.newView(new EntityNameFilter(".svn").not()); // Create a read/write file system in a temporary directory to use for putting // built files in. Keep this directory when the script is done (set keep flag to // true). targetRoot = TempFileUtil.createTempDirectory( "myproject", null, true); // Set a temporary directory on the target file system. Tasks that need to // create temporary files use this directory for them. targetRoot.getFileSystem().setTemporaryFilesDirectory( Directories.newDirectory(targetRoot, "tmp"));

JRuby

# Create a read only file system with its root directory in # /home/me/myproject/src srcd = Schmant::SchmantFileSystems.getEntityForDirectory( Java::JavaIo::File.new("/home/me/myproject/src"), true) # Get a view of the root directory that hides all .svn directories. When this # view is used for getting child directories, all .svn directories will be # hidden in them too, making all .svn directories in the file system invisible # for all EntityFS-aware tasks. srcRoot = srcd.newView(Schmant::EntityNameFilter.new(".svn").not) # Create a read/write file system in a temporary directory to use for putting # built files in. Keep this directory when the script is done (set keep flag to # true). targetRoot = Schmant::TempFileUtil.createTempDirectory( "myproject", nil, true) # Set a temporary directory on the target file system. Tasks that need to # create temporary files use this directory for them. targetRoot.fileSystem.setTemporaryFilesDirectory( Schmant::Directories.newDirectory(targetRoot, "tmp"))

Jython

# Create a read only file system with its root directory in # /home/me/myproject/src srcd = SchmantFileSystems.getEntityForDirectory( \ File("/home/me/myproject/src"), True) # Get a view of the root directory that hides all .svn directories. When this # view is used for getting child directories, all .svn directories will be # hidden in them too, making all .svn directories in the file system invisible # for all EntityFS-aware tasks. srcRoot = srcd.newView(EntityNameFilter(".svn").not()) # Create a read/write file system in a temporary directory to use for putting # built files in. Keep this directory when the script is done (set keep flag to # true). targetRoot = TempFileUtil.createTempDirectory( \ "myproject", None, True) # Set a temporary directory on the target file system. Tasks that need to # create temporary files use this directory for them. targetRoot.getFileSystem().setTemporaryFilesDirectory( \ Directories.newDirectory(targetRoot, "tmp"))

This chapter contains a few useful tips for troubleshooting build script errors.

When a build script fails, it prints an error message and one or more stack traces. Most errors are logged twice because many script language implementations do not propagate all error information in the exceptions that they receive. This results in a very long list of stack traces. The rule of thumb is that the most useful stack trace information is logged first.

When faced with an error that is non-trivial to diagnose, what can you do? Here follows a few tips:

A task can be made to trace log its configuration before it is run. Trace logging can either be enabled in three different ways:

  1. For all tasks by setting the traceMode property of TraceMode or by using the --trace flag when launching Schmant.
  2. For all tasks created by a task factory by setting the task factory's traceLogging property.
  3. For one task by settings its traceLogging property.

By default, Schmant deletes all temporary directories created using the TempFileUtil when it exits. If Schmant is launched with the -k flag, temporary files and directories are not deleted.

Argument interpretation gives great flexibility in which kinds of arguments that can be used for setting task properties. Most tasks use ArgumentInterpreter together with an ArgumentInterpretationStrategy to interpret some of their properties. This appendix describes how the different strategies interpret properties into the expected types. It also lists useful implementations of the different argument types.

Unless a custom interpretation method or class is used, arguments are interpreted using the ArgumentInterpreter.interpret(Object, org.schmant.arg.ArgumentIntepretationStrategy) method. The first argument to that method is one object or an array or collection of objects. These objects are the objects to interpret. The second argument to the method is the ArgumentInterpretationStrategy to use

First the interpret method flattens the object collection (using FlatteningList), and all closures in the collection are run. Then the produced objects are fetched from all Producer:s in the collection. The resulting argument list is fed into the ArgumentInterpretationStrategy object, which interprets the list using its strategy and returns an ArgumentInterpretationResult object containing all interpreted objects and the objects that it was not able to interpret.

When it is created, the ArgumentInterpretationStrategy object can be configured to treat different situations as errors or not. If it encounters a situation that it has been configured to treat as an error it will throw an ArgumentInterpretationException. These two configuration modes are supported by all strategy objects:

ArgumentInterpretationStrategy.ALLOW_NOT_INTERPRETED

Tell the strategy object that it is not an error if it cannot interpret all objects. All objects that it cannot interpret along with their interpretation traces that describes how the strategy tried to interpret them are returned in the ArgumentInterpretationResult object.

ArgumentInterpretationStrategy.ALLOW_ONE_AND_ONLY_ONE_RESULT_OBJECT

Allow only one result object. If the argument list is interpreted into more than one object, this results in an error.

The following sections describe how the different ArgumentInterpretationStrategy objects interpret objects into different types.

InterpretAsFileStrategy is used to interpret objects into File:s (files and/or directories).

java.io.File are used by tasks that use modules that are not EntityFS-aware, i.e. mostly tasks that run external programs. The files returned by the interpretation methods may or may not exist. Their paths may be relative or absolute.

First, ArgumentInterpreter preprocesses the argument list as described above. Then the strategy object tries to interpret each object in the preprocessed list by checking if each object is of any of the types listed below, in the order that they are listed. If it is, the algorithm listed at the matched type is used to create the object tor return. If there are objects for which there are no matches, the configuration of the ArgumentInterpretationStrategy decides what will happen.


InterpretAsFileDirectoryStrategy is used to interpret objects into directory File:s.

This method uses the results from a InterpretAsFileStrategy object, but throws an ArgumentInterpretationException if the result is not a directory.

InterpretAsUrlStrategy is used to interpret objects into URL objects.

First, ArgumentInterpreter preprocesses the argument list as described above. Then the strategy object tries to interpret each object in the preprocessed list by checking if each object is of any of the types listed below, in the order that they are listed. If it is, the algorithm listed at the matched type is used to create the object tor return. If there are objects for which there are no matches, the configuration of the ArgumentInterpretationStrategy decides what will happen.


InterpretAsFutureEntityStrategy is used to interpret objects into FutureEntity objects.

The InterpretAsFutureEntityStrategy constructor takes an optional source value. It can be used as a hint to a FutureEntityStrategy object to help it decide what to call the entity it creates. It is up to each task that use future entities to decide if it should provide a hint. A process task uses the source property as a hint.

First, ArgumentInterpreter preprocesses the argument list as described above. Then the strategy object tries to interpret each object in the preprocessed list by checking if each object is of any of the types listed below, in the order that they are listed. If it is, the algorithm listed at the matched type is used to create the object tor return. If there are objects for which there are no matches, the configuration of the ArgumentInterpretationStrategy decides what will happen.


InterpretAsNamedReadableFileStrategy is used to interpret objects into NamedReadableFile:s.

First, ArgumentInterpreter preprocesses the argument list as described above. Then the strategy object tries to interpret each object in the preprocessed list by checking if each object is of any of the types listed below, in the order that they are listed. If it is, the algorithm listed at the matched type is used to create the object tor return. If there are objects for which there are no matches, the configuration of the ArgumentInterpretationStrategy decides what will happen.


See CopyTF examples for how to use an URL as a NamedReadableFile.

InterpretAsReadableFileStrategy is used to interpret objects into ReadableFile objects.

First, ArgumentInterpreter preprocesses the argument list as described above. Then the strategy object tries to interpret each object in the preprocessed list by checking if each object is of any of the types listed below, in the order that they are listed. If it is, the algorithm listed at the matched type is used to create the object tor return. If there are objects for which there are no matches, the configuration of the ArgumentInterpretationStrategy decides what will happen.


See CopyTF examples for how to use an URL as a ReadableFile.

InterpretAsWritableFileStrategy is used to interpret objects into WritableFile objects representing existing files.

First, ArgumentInterpreter preprocesses the argument list as described above. Then the strategy object tries to interpret each object in the preprocessed list by checking if each object is of any of the types listed below, in the order that they are listed. If it is, the algorithm listed at the matched type is used to create the object tor return. If there are objects for which there are no matches, the configuration of the ArgumentInterpretationStrategy decides what will happen.


The InterpretAsNewWritableFileStrategy interprets an object into a location for a new writable file, creates the file and then returns it. If there already is a file at the target location, it uses an OverwriteStrategy to decide what to do with it. If the strategy decides to keep the old file, most tasks will fail.

First, ArgumentInterpreter preprocesses the argument list as described above. Then the strategy object tries to interpret each object in the preprocessed list by checking if each object is of any of the types listed below, in the order that they are listed. If it is, the algorithm listed at the matched type is used to create the object tor return. If there are objects for which there are no matches, the configuration of the ArgumentInterpretationStrategy decides what will happen.


InterpretAsEntityHolderStrategy is used to interpret objects into EntityHolder objects.

An entity holder is a very limited Directory.

The returned directory is wrapped in a DirectoryRepresentation object. It contains a DirectoryView and an optional EntityFilter. If the filter is set, the running task will only see the entities matching the filter. If the task is recursive, it will still propagate down into all subdirectories, even if they don't match the filter. This is how using a DirectoryRepresentation source differs from using a DirectoryView source; when using a DirectoryView source, only the subdirectories matching the filter will be processed.

First, ArgumentInterpreter preprocesses the argument list as described above. Then the strategy object tries to interpret each object in the preprocessed list by checking if each object is of any of the types listed below, in the order that they are listed. If it is, the algorithm listed at the matched type is used to create the object tor return. If there are objects for which there are no matches, the configuration of the ArgumentInterpretationStrategy decides what will happen.


InterpretAsReadOnlyEntityStrategy is used to interpret objects as read only EntityView:s.

By calling this method, the client says that it does not require a entity that it can write to, only one it can read from. The returned entity is not required to be read only, it might just as well be read/write.

First, ArgumentInterpreter preprocesses the argument list as described above. Then the strategy object tries to interpret each object in the preprocessed list by checking if each object is of any of the types listed below, in the order that they are listed. If it is, the algorithm listed at the matched type is used to create the object tor return. If there are objects for which there are no matches, the configuration of the ArgumentInterpretationStrategy decides what will happen.


InterpretAsReadOnlyDirectoryStrategy is used to interpret objects as read only DirectoryView:s.

By calling this method, the client says that it does not require a directory that it can write to, only one it can read from. The returned directory is not required to be read only, it might just as well be read/write.

The returned directory is wrapped in a DirectoryRepresentation object. It contains a DirectoryView and an optional EntityFilter. If the filter is set, the running task will only see the entities matching the filter. If the task is recursive, it will still propagate down into all subdirectories, even if they don't match the filter. This is how using a DirectoryRepresentation source differs from using a DirectoryView source; when using a DirectoryView source, only the subdirectories matching the filter will be processed.

First, ArgumentInterpreter preprocesses the argument list as described above. Then the strategy object tries to interpret each object in the preprocessed list by checking if each object is of any of the types listed below, in the order that they are listed. If it is, the algorithm listed at the matched type is used to create the object tor return. If there are objects for which there are no matches, the configuration of the ArgumentInterpretationStrategy decides what will happen.


InterpretAsDirectoryStrategy is used to interpret objects as read/write DirectoryView:s.

The returned directory is wrapped in a DirectoryRepresentation object. It contains a DirectoryView and an optional EntityFilter. If the filter is set, the running task will only see the entities matching the filter. If the task is recursive, it will still propagate down into all subdirectories, even if they don't match the filter. This is how using a DirectoryRepresentation source differs from using a DirectoryView source; when using a DirectoryView source, only the subdirectories matching the filter will be processed.

First, ArgumentInterpreter preprocesses the argument list as described above. Then the strategy object tries to interpret each object in the preprocessed list by checking if each object is of any of the types listed below, in the order that they are listed. If it is, the algorithm listed at the matched type is used to create the object tor return. If there are objects for which there are no matches, the configuration of the ArgumentInterpretationStrategy decides what will happen.


InterpretAsXmlSourceStrategy is used to interpret objects as XML Source objects that can be used for XSL transformations.

First, ArgumentInterpreter preprocesses the argument list as described above. Then the strategy object tries to interpret each object in the preprocessed list by checking if each object is of any of the types listed below, in the order that they are listed. If it is, the algorithm listed at the matched type is used to create the object tor return. If there are objects for which there are no matches, the configuration of the ArgumentInterpretationStrategy decides what will happen.


A ReadableFile is a container for file data. It can be opened and read from any number of times.

The tree view below lists the type's inheritance hierarchy. The names of interfaces are written in italics.

ReadableFile                 -- container for file data that can be opened and read from
+ ByteArrayReadableFile      -- file that reads data from a byte array
+ CharSequenceReadableFile   -- file that reads data from a CharSequence
|                               (a String, for instance)
+ BZip2ReadableFile          -- file that transparently decompresses data when read.
+ GZipReadableFile           -- file that transparently decompresses data when read.
+ GZipReadableFileProxy      -- file that transparently decompresses data when read.
+ LzmaReadableFile           -- file that transparently decompresses data when read.
+ NamedReadableFile          -- a ReadableFile with a name
  + ReadWritableFile         -- a read/write file
  | + ReadWritableFileAdapter -- Adapter from a File to a ReadWritableFile
  | + EFile                  -- a file entity
  + ManualNamedReadableFile  -- named file that reads data from a byte array
  |                             or a CharSequence (such as a String)
  + NamedReadableFileAdapter -- adapter for turning a ReadableFile
  |                             into a NamedReadableFile
  + ReadWritableFileAdapter  -- adapter for turning a File into a
  |                             NamedReadableFile
  + UrlReadableFile          -- named file that reads data from the target of 
                                a URL

Since ReadableFile is a very small interface, it is easy to do a custom implementation of it if none of the already existing implementations suffice.

A NamedReadableFile is a ReadableFile with a name. Any ReadableFile can be adapted to a NamedReadableFile by using the NamedReadableFileAdapter.

The tree view below lists the type's inheritance hierarchy. The names of interfaces are written in italics.

NamedReadableFile           -- a ReadableFile with a name
+ ReadWritableFile          -- a read/write file
| + ReadWritableFileAdapter -- Adapter from a File to a ReadWritableFile
| + EFile                   -- a file entity
+ ManualNamedReadableFile   -- named file that reads data from a byte array
|                              or a CharSequence (such as a String)
+ NamedReadableFileAdapter  -- adapter for turning a ReadableFile
|                              into a NamedReadableFile
+ ReadWritableFileAdapter   -- adapter for turning a File into a
|                              NamedReadableFile
+ UrlReadableFile           -- named file that reads data from the target of 
                               a URL

Since NamedReadableFile is a very small interface, it is easy to do a custom implementation of it if none of the already existing implementations suffice.

A WritableFile is a sink for data. It can be opened for writing to or appending to any number of times.

WritableFile                            -- container for file data that can be opened and written to
+ BZip2ExistingWritableFileProxy        -- file that transparently compresses data written to it.
+ BZip2NewWritableFileProxy             -- file that transparently compresses data written to it.
+ BZip2WritableFile                     -- file that transparently compresses data written to it.
+ FileWritableFile                      -- adapter from a File to a WritableFile
+ GZipExistingWritableFileProxy         -- file that transparently compresses data written to it.
+ GZipNewWritableFileProxy              -- file that transparently compresses data written to it.
+ GZipWritableFile                      -- file that transparently compresses data written to it.
+ LzmaExistingWritableFileProxy         -- file that transparently compresses data written to it.
+ LzmaNewWritableFileProxy              -- file that transparently compresses data written to it.
+ LzmaWritableFile                      -- file that transparently compresses data written to it.
+ MultiplexingExistingWritableFileProxy -- file that writes to one or more proxied files.
+ MultiplexingNewWritableFileProxy      -- file that writes to one or more proxied files.
+ ReadWritableFile                      -- a read/write file
  + ReadWritableFileAdapter             -- Adapter from a File to a ReadWritableFile
  + EFile                               -- a file entity

Since WritableFile is a very small interface, it is easy to do a custom implementation of it if none of the already existing implementations suffice.

A RandomlyAccessibleFile is a data container that may a client may open for random access. It may be read/write or it may be read only.

RandomlyAccessibleFile      -- container for file data that can be opened for random access
+ ByteArrayReadableFile     -- Read only container for bytes.
+ ReadWritableFile          -- a read/write file
  + ReadWritableFileAdapter -- Adapter from a File to a ReadWritableFile
  + EFile                   -- a file entity

An EntityView is a file system entity, either a file or a directory. It has a unique location in a FileSystem.

The tree view below lists the type's inheritance hierarchy. The names of interfaces are written in italics.

EntityView      -- view of generic file system entity
+ Entity        -- generic file system entity
| + EFile       -- file entity
| + Directory   -- directory entity (no filters)
+ DirectoryView -- Directory with contents filtered by zero or more
  |                EntityFilter:s
  + Directory   -- directory entity (no filters)

The FutureEntityStrategy defines a strategy that uses a source object and creates a FutureEntity from it. Note that you can use a closure instead of a future entity strategy.

The tree view below lists the type's inheritance hierarchy. The names of interfaces are written in italics.

FutureEntityStrategy         -- future entity strategy
+ FutureEntityIndexStrategy  -- strategy that creates future entities on the
                                form [base_directory]/[prefix][index][suffix]
                                where index is incremented for each created
                                future entity

Schmant uses EntityFS extensively for working with files and directories. This appendix gives some basic information about EntityFS programming that might be useful for build script writers. More exhaustive information about EntityFS programming can be found in the EntityFS Programmer's Guide.

EntityFS is an object-oriented file system API for Java. The difference in philosophy between EntityFS and Java's built in file and directory support is that Java's File object represents a path in the local file system, while EntityFS entities represent the files and directories themselves. From the application's perspective, an EntityFS EFile entity, for instance, can be said to be the file, while a Java File object is a pointer to the file. If the EntityFS EFile is moved, the object is updated with the new location. If it is deleted, the entity object is invalidated. If a new file is put in the same location in the file system, the original EFile object is still invalid.

The properties of entity objects are further discussed in this article on the EntityFS site.

EntityFS file systems can be built on the operating system's file system, on Zip or Jar files or in memory. See for instance the section called “In-memory file systems”.

An EntityFS FileSystem contains file and directory entities. Entities are mostly used through any of the utility classes.

The EntityFS Programmer's Guide contains an appendix on how to move from EntityFS classes to java.io classes and back again. See also Example 5.3, “From Java files to EntityFS and back again”.

The SchmantFileSystems utility class contains the makeFileBacked and makeRandomlyAccessible methods for making EFile entities File-backed and FCRandomAccess:ible, respectively.

Entities always live within the context of a FileSystem. It has some global properties, such as strategies for entity locking and for handling of file system events. It can also have any number of FileSystemCapability:s and EntityCapability:s that extend its functionality. For instance, the ECFileResolvable entity capability makes files and directories resolvable to Java's File objects.

A file system always contains a root Directory that can be retrieved by calling its getRootDirectory() method.

File systems are either created manually using a FileSystemBuilder or through any of the utility methods of SchmantFileSystems. SchmantFileSystems adapts the file system-creating methods of EntityFS' FileSystems and also adds a few file system-creation methods of its own.

FileSystem:s may be created to support entity locking. This should be enabled for all read/write file systems accessed by parallel build threads, since it prevents tasks from accidentally writing to the same files or directories simultaneously. Many of the file systems returned by SchmantFileSystems' methods have locking enabled.


Example B.1. Creating source and target file systems

Groovy

import java.io.File import org.entityfs.util.Directories import org.entityfs.util.filter.entity.EntityNameFilter import org.schmant.support.entityfs.SchmantFileSystems import org.schmant.support.io.TempFileUtil // Create a read only file system with its root directory in // /home/me/myproject/src srcd = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/src"), true) // Get a view of the root directory that hides all .svn directories. When this // view is used for getting child directories, all .svn directories will be // hidden in them too, making all .svn directories in the file system invisible // for all EntityFS-aware tasks. // The ~ is used to negate the filter. srcRoot = srcd.newView(~(new EntityNameFilter(".svn"))) // Create a read/write file system in a temporary directory to use for putting // built files in. Keep this directory when the script is done (set keep flag to // true). targetRoot = TempFileUtil.createTempDirectory( "myproject", null, true) // Set a temporary directory on the target file system. Tasks that need to // create temporary files use this directory for them. targetRoot.fileSystem.setTemporaryFilesDirectory( Directories.newDirectory(targetRoot, "tmp"))

JavaScript

// Create a read only file system with its root directory in // /home/me/myproject/src srcd = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/src"), true); // Get a view of the root directory that hides all .svn directories. When this // view is used for getting child directories, all .svn directories will be // hidden in them too, making all .svn directories in the file system invisible // for all EntityFS-aware tasks. srcRoot = srcd.newView(new EntityNameFilter(".svn").not()); // Create a read/write file system in a temporary directory to use for putting // built files in. Keep this directory when the script is done (set keep flag to // true). targetRoot = TempFileUtil.createTempDirectory( "myproject", null, true); // Set a temporary directory on the target file system. Tasks that need to // create temporary files use this directory for them. targetRoot.getFileSystem().setTemporaryFilesDirectory( Directories.newDirectory(targetRoot, "tmp"));

JRuby

// Create a read only file system with its root directory in // /home/me/myproject/src srcd = SchmantFileSystems.getEntityForDirectory( new File("/home/me/myproject/src"), true); // Get a view of the root directory that hides all .svn directories. When this // view is used for getting child directories, all .svn directories will be // hidden in them too, making all .svn directories in the file system invisible // for all EntityFS-aware tasks. srcRoot = srcd.newView(new EntityNameFilter(".svn").not()); // Create a read/write file system in a temporary directory to use for putting // built files in. Keep this directory when the script is done (set keep flag to // true). targetRoot = TempFileUtil.createTempDirectory( "myproject", null, true); // Set a temporary directory on the target file system. Tasks that need to // create temporary files use this directory for them. targetRoot.getFileSystem().setTemporaryFilesDirectory( Directories.newDirectory(targetRoot, "tmp"));

Jython

# Create a read only file system with its root directory in # /home/me/myproject/src srcd = SchmantFileSystems.getEntityForDirectory( \ File("/home/me/myproject/src"), True) # Get a view of the root directory that hides all .svn directories. When this # view is used for getting child directories, all .svn directories will be # hidden in them too, making all .svn directories in the file system invisible # for all EntityFS-aware tasks. srcRoot = srcd.newView(EntityNameFilter(".svn").not()) # Create a read/write file system in a temporary directory to use for putting # built files in. Keep this directory when the script is done (set keep flag to # true). targetRoot = TempFileUtil.createTempDirectory( \ "myproject", None, True) # Set a temporary directory on the target file system. Tasks that need to # create temporary files use this directory for them. targetRoot.getFileSystem().setTemporaryFilesDirectory( \ Directories.newDirectory(targetRoot, "tmp"))

Schmant implements a LogAdapter, the SchmantReportLogAdapter, for making EntityFS log into a Schmant Report. This log adapter can manually be set on a file system fs by calling fs.getLogAdapterHolder().setLogAdapter(SchmantReportLogAdapter.INSTANCE). If any of the methods of SchmantFileSystems or TempFileUtil are used for creating entities (and thus also file systems), the returned file systems already have the Schmant log adapter set.

Example B.2. Using the SchmantReportLogAdapter

Groovy

import org.entityfs.fs.FSRWFileSystemBuilder import org.schmant.report.SchmantReportLogAdapter // Create a new file system with its root directory in the directory d // (a File). def fs = new FSRWFileSystemBuilder(). setRoot(d). create() // Set the Schmant log adapter fs.logAdapterHolder.setLogAdapter( SchmantReportLogAdapter.INSTANCE) // This is written to the current Report fs.logAdapter.logWarning("This is a warning message!")

JavaScript

// Create a new file system with its root directory in the directory d // (a File). fs = new FSRWFileSystemBuilder(). setRoot(d). create(); // Set the Schmant log adapter fs.getLogAdapterHolder().setLogAdapter( SchmantReportLogAdapter.INSTANCE); // This is written to the current Report fs.getLogAdapter().logWarning("This is a warning message!");

JRuby

# Create a new file system with its root directory in the directory d # (a File). fs = Schmant::FSRWFileSystemBuilder.new. setRoot($d). create # Set the Schmant log adapter fs.logAdapterHolder.setLogAdapter( Schmant::SchmantReportLogAdapter::INSTANCE) # This is written to the current Report fs.logAdapter.logWarning("This is a warning message!")

Jython

# Create a new file system with its root directory in the directory d # (a File). fs = FSRWFileSystemBuilder(). \ setRoot(d). \ create() # Set the Schmant log adapter fs.getLogAdapterHolder().setLogAdapter( \ SchmantReportLogAdapter.INSTANCE) # This is written to the current Report fs.getLogAdapter().logWarning("This is a warning message!")

Entity:s live within the context of a FileSystem; an entity can never leave it. Every entity has a unique location, an AbsoluteLocation in its file system.

Entity objects are never instantiated directly by clients. Existing entities are retrieved from their parent Directory object or, more commonly, by using the utility methods of Directories. New entities are also created by using the methods of their parent Directory or by using Directories' methods.

All entities use read/write locking to protect them from concurrent modification. Before using an entity method on an entity in a locking file system, the entity has to be either locked for reading or writing, depending on what the method does. All utility class methods acquire the necessary locks before calling entity methods, which make them much easier to use compared with calling entity methods directly.

A DirectoryView is a Directory viewed through zero or more EntityFilter:s. If a child directory is fetched through a directory view, it inherits the parent directory's view settings.

A common usage is to use directory views to hide all .svn directories in a file system, see this example.

The following table contains the entity Filter implementations that EntityFS comes with.

Table B.3. EntityFilter implementations

ImplementationDescription
DirectoryEmptyFilterMatches empty directories.
DirectoryFilterMatches directories.
EFileFilterMatches files.
EFileNameExtensionFilterMatches files whose names have one of a set of extensions.
EntityIdentityFilterMatches a specific entity instance.
EntityLatestModificationTimeFilterMatches entities that were modified before a specific time.
EntityLocationGlobFilterMatches entities whose location relative to a base directory matches a glob pattern.
EntityLocationRegexpFilterMatches entities whose location relative to a base directory matches a regular expression pattern.
EntityNameFilterMatches entities with a specific name.
EntityNameGlobFilterMatches entities whose names match a glob pattern.
EntityNamePrefixFilterMatches entities whose names start with a specific prefix.
EntityNameRegexpFilterMatches entities whose names match a regular expression pattern.
EntityRecentModificationFilterMatches entities that have been modified within a specified time limit.
EntityTypeFilterMatches entities of a specific type (files or directories). It is better to use EFileFilter or DirectoryFilter instead.
FalseFilterDoes not match any entity.
LockedEntityFilterMatches entities that are locked for reading or writing by the current thread.
ParentFilterMatches entities that have a specific parent directory.
ReadLockedEntityFilterMatches entities that are locked for reading by the current thread.
SuperParentAndFilterMatches an entity if all of its parent directories match another filter.
SuperParentAndFilterMatches an entity if any of its parent directories match another filter.
TrueFilterMatches all entities.
WriteLockedEntityFilterMatches entities that are locked for writing by the current thread.

All entity filters implement ConvenientFilter so they have and, or, xor and not methods to combine them with other filters.

See for instance this example for how filters can be used.

The following is a collection of best practices for using EntityFS in Schmant build scripts:

  1. If possible, use SchmantFileSystems methods instead of FileSystemBuilder:s for creating file systems. The file systems created by SchmantFileSystems are configured with settings that work well with build scripts.

  2. Create a read only file system for the build's source files and a read/write file system for the build's target directories. See Example B.1, “Creating source and target file systems”.