Blog-Archiv

Sonntag, 31. Mai 2020

Compiling Java Modules from Command Line

If you want to check your IDE whether it handles Java modules accurately, then you need to do a command line compilation and see what the JDK's javac compiler says. But how to do this?

Compilation Commands

Since Java 9, the javac compiler supports two kinds of module compilations: single- and multi-module.

Single Module

Assume following source directories and files:

fri.i18n.messages
module-info.java
fri
i18n
messages
I18nMessage.java
Messages.java

When your terminal window stands above the module's root directory fri.i18n.messages, following command-line would compile the module's classes into the target directory given by the -d option, in this case I named it module-classes:

javac --module-path module-classes \
    -d module-classes/fri.i18n.messages \
    `find fri.i18n.messages -name '*.java'`

The --module-path option (also -p) is necessary only when the module depends on other modules that were already compiled into module-classes. It tells the compiler where to search for referenced modules, being the future replacement for CLASSPATH.

The -d option gives the output directory where to write compiled classes into. In this case the name of the module (fri.i18n.messages) must be appended to achieve a correct output path.

The find command is available on UNIX systems only, on WINDOWS you have to download and install it. It searches all .java files inside the fri.i18n.messages directory. The `backquotes` are a UNIX-shell mechanism that puts the output of the find command into the javac command line. (Not a good solution, because the list of sources could get huge. See below for a better way to pass sources to the compiler. The new --module-source-path option was not made for this!)

This single-module compilation command would result in following:

fri.i18n.messages
module-info.java
fri
i18n
messages
I18nMessage.java
Messages.java
module-classes
fri.i18n.messages
module-info.class
fri
i18n
messages
I18nMessage.class
Messages.class

Multiple Modules

A certain directory structure is required to compile several modules in one run. All modules must be located below a directory that is given as --module-source-path, in this case I named it module-sources. Each module's sources are in a directory below that is named like the module. Mind that this name is duplicated in module-info.java!

module-sources
fri.text.output
module-info.java
fri
text
output
Output.java
fri.text.format
module-info.java
fri
text
format
Format.java

You can compile all modules contained in module-sources by following command line:

javac --module-path module-classes \
    -d module-classes \
    --module-source-path module-sources \
    `find module-sources -name '*.java'`

Again the --module-path option could be left out when the modules do not depend on precompiled modules.

The --module-source-path option points to the directory where the module directories are below, in this case it is module-sources.

Result of this compilation command would be:

module-sources
fri.text.output
module-info.java
fri
text
output
Output.java
fri.text.format
module-info.java
fri
text
format
Format.java
module-classes
fri.text.output
module-info.class
fri
text
output
Output.class
fri.text.format
module-info.class
fri
text
format
Format.class

Mind that I didn't have to put any module name into the command line, like I had to do for a single module compilation.

Huge List of Sources

Putting the find command as substitution into the javac command line is not a real-world solution. Most projects have tons of source files, and the shell command buffer may get out of bounds. There is an option to replace the list of sources by a file name tagged with '@'. You would let the find command write into a file, and then pass the file to javac. Here is an example for single module compilation:

find fri.i18n.messages -name '*.java' >sources.list
javac --module-path module-classes -d module-classes/fri.i18n.messages @sources.list
rm -f sources.list

The according multi-module commands would be:

find module-sources -name '*.java' >sources.list
javac --module-path module-classes -d module-classes --module-source-path module-sources @sources.list
rm -f sources.list

The '@' (ampersand) before the file name sources.list does the magic.

Automation Script

Here is a UNIX shell script, named modules-compile.sh, that can do both single and multi module compilations. Put this into the directory above your module source directory like the following:

fri.i18n.messages
module-info.java
fri
i18n
messages
I18nMessage.java
Messages.java
module-sources
fri.text.output
module-info.java
fri
text
output
Output.java
fri.text.format
module-info.java
fri
text
format
Format.java
module-classes
modules.list
modules-compile.sh

Put the list of your module directories into modules.list (no comments allowed here!):

fri.i18n.messages
module-sources

These can be single modules (fri.i18n.messages) or directories containing multiple modules (module-sources).
IMPORTANT: keep the list in dependency order, so that basic modules are already compiled when the modules that depend on them get compiled!

Then put following into modules-compile.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# compiled classes target directory
moduleTargetDir=module-classes

srclist=sources.list

# sources must be in dependency order!
moduleDirs=`cat modules.list`

for dir in $moduleDirs
do
  # try to find module-info.java in given directory
  moduleInfo=`find \$dir -name module-info.java`
  [ -z "$moduleInfo" ] && {
    echo "ERROR: Ignoring $dir because it contains no module-info.java!" >&2
    continue
  }
 
  # check how many module-info.java files were found
  modulesCount=`echo \$moduleInfo | wc -w`
  if [ "$modulesCount" = 1 ]
  then
    # try to extract the module name from module-info.java
    moduleName=`awk '/^[ \t]*module/ {print $2}' \$moduleInfo`
    [ -z "$moduleName" ] && {
      echo "ERROR: Could not extract module name from $moduleInfo" >&2
      continue
    }
    srcdir=`dirname \$moduleInfo`
  
    echo "Compiling module $moduleName in $srcdir to $moduleTargetDir ..." >&2
  else
    # the grandparent directory of first module-info.java must be the parent of all
    firstModulesDir=`echo \$moduleInfo | awk '{print $1}'`
    srcdir=`dirname \$firstModulesDir`
    srcdir=`dirname \$srcdir`
  
    echo "Multimodule compile in $srcdir to $moduleTargetDir ...." >&2
  fi

  # write sources list into file and use the javac @sources.list option
  find $srcdir -name '*.java' >$srclist
 
  if [ "$modulesCount" = 1 ]
  then
    javac --module-path $moduleTargetDir -d $moduleTargetDir/$moduleName @$srclist
  else
    javac --module-path $moduleTargetDir -d $moduleTargetDir --module-source-path $srcdir @$srclist
  fi
 
  rm -f $srclist
done

Most likely you'll have to make the script executable then:

chmod 754 modules-compile.sh

Mind that this script requires the name of the module written in same line as the module keyword in a module-info.java of a single module (script line 23). For multi-module compilation this is not needed.

This script works with both Maven and Eclipse projects. Eclipse projects have all sources inside a src/ directory, Maven has them in src/main/java/ and src/test/java/.

It would take too much space to explain the script. Shell scripts are not well readable, they are quick & dirty, you need an expert to maintain them. But they are quite useful sometimes, so try it out !-)




Keine Kommentare: