1. Introduction

We can independently create and run a simple Java class with just a Java installation. However, including third-party dependencies usually requires a build tool like Maven or Gradle, which can be cumbersome for small and straightforward Java applications. Packaging a Java app as an executable also requires adding plugins to these build tools or using separate tools.

Fortunately, we can simplify Java app development for smaller projects using a practical Scala tool called Scala-CLI. In this tutorial, we’ll explore Scala-CLI’s features and how to use it for tasks like compiling, building, packaging, and using specific JDKs.

2. What Is Scala-CLI?

Scala-CLI is a command-line tool primarily designed for building Scala applications. It simplifies compiling, running, and packaging Scala applications, eliminating the need for a comprehensive build system.

Interestingly, Scala-CLI can also build pure Java applications, leveraging its full capabilities to streamline development.

3. Set-Up

Scala-CLI can be installed by following the instructions provided on the official website. We can verify if the installation is successful by using the following command:

scala-cli --version

If the installation is successful, it prints the scala-cli version to the console.

Furthermore, we can install the Metals extension in VS Code Editor to work inside an IDE. Scala-CLI doesn’t need a pre-installed JDK on the machine, as it can manage the JDK itself.

However, sometimes, there might be a conflict with the other Java plugins. If there is an issue with error reporting in VS Code, we might need to disable the other Java extension temporarily.

We’ll also create a base directory named scala-cli for this tutorial, where we keep all the files related to this tutorial.

4. Hello World Program

As is the norm, let’s start with a simple Hello World program. First, we can create a new hello-world directory under the base directory. Next, let’s create a file named HelloWorld.java within this directory with the following code:

package com.baeldung;
public class HelloWorld {
    public static void main(String args[]) {
        System.out.println("Hello, World!");
    }
}

We can run this simple Java class using the command:

scala-cli HelloWorld.java

On the first run, it downloads the necessary libraries for it to run. If no JDK is installed, Scala-CLI automatically downloads the Open JDK and uses it for compilation. If there is already one available, then it uses that particular JDK. While writing this article, Scala-CLI uses the JDK 17 as the default version. However, it might upgrade to newer versions in the future. After that, it runs this class and prints the given text to the console:

Hello World Output

If we open this directory in VS Code, we might see compilation errors and need proper navigation ability. The VS Code might be able to apply navigation and code completion in this case; however, when we use third-party dependencies, it won’t resolve them. To support it, we can create the necessary metadata for the Metals plugins by using the command inside the hello-world directory:

scala-cli setup-ide .

This generates the necessary metadata required by the Metals plugin for advanced features.

The Metals plugin also provides a tooltip to run the main class directly from the VS Code IDE:

VS Code Metals Run

We can click on the run action, and it executes the program.

5. Directives

In the previous section, we ran a simple program using Scala-CLI. However, the same can be done with the java command, and doesn’t need another tool. Now, let’s look at some of Scala-CLI’s most useful features.

Directives are metadata settings that have special meanings in Scala-CLI. The directives start with the special syntax //>. They must be placed before any Java code, even before the package statement. In this section, let’s explore some of the useful directives in Scala-CLI.

5.1. Specifying Java Version

In the previous sample code, we didn’t specify any Java version, and it uses either the default JDK or the already installed JDK for the execution. However, we can configure a specific version of JDK we want the code to use. For that, we use the jvm directive.

Let’s write a sample code that uses JDK 21. We can name the file as Directives.java and place it under scala-cli/jdk-config path:

//> using jvm 21
package com.baeldung;
record Greet(String name){};
public class Jdk21Sample {
    public static void main(String args[]) {
        var greet = new Greet("Baeldung");
        var greeting = "Hello, " + greet.name();
        System.out.println(greeting);
    }
}

Notice the directive jvm being used with the required version of JDK. This means the Scala-CLI uses JDK 21 to execute the entire file.

By default, it uses Adoptium Open JDK of the specified version. We can also select another flavor of JDK explicitly in the directive. For example, to use Zulu JDK, we can use the directive:

//> using jvm zulu:21

When we run this directive, Scala-CLI downloads the Zulu JDK and executes the program using it. Without any manual setup, we can easily switch between different JDKs and execute the programs. Under the hood, Scala-CLI uses the tool Coursier to manage the dependencies and the JDK versions.

5.2. Passing Javac Options and Java Properties

We can use directives to pass Java options to the compiler. Let’s look at an example:

//> using jvm 21
//> using javaOpt -Xmx2g, -DappName=baeldungApp, --enable-preview
//> using javaProp language=english, country=usa
//> using javacOpt --release 21 --enable-preview
public class JavaArgs {
    public static void main(String[] args) {
        String appName = System.getProperty("appName");
        String language = System.getProperty("language");
        String country = System.getProperty("country");
        String combinedStr = STR."appName = \{ appName } , language = \{ language } and country = \{ country }";
        System.out.println(combinedStr);
    }
}

The code above passes three different types of options. We can use the directive javaOpt to pass the JVM arguments such as -Xmx or system properties. Similarly, we can use javacOpt to pass options to the javac compiler used during the compilation. Furthermore, we can also pass Java properties to the application by using the directive javaProp.

These directives enable configuring and controlling arguments directly within the main Java class.

5.3. Managing External Dependencies

We can manage external dependencies using the directive dep. To demonstrate this, let’s add a simple library. We can create a new file named DependencyApp.java and paste the following content:

//> using dep com.google.code.gson:gson:2.8.9
import com.google.gson.JsonParser;
import com.google.gson.JsonElement;
public class DependencyApp {
    public static void main(String args[]) {
        String jsonString = "{\"country\": \"Germany\", \"language\": \"German\", \"currency\": \"Euro\"}";
        var countryJson = JsonParser.parseString(jsonString);
        var country = countryJson.getAsJsonObject().get("country").getAsString();
        System.out.println("Selected country: " + country);
    }
}

In the code above, we added the dependency for Google Gson using the directive dep. Scala-CLI uses the gradle style syntax for dependency definition.

When we execute the t code, we get the output as:

Compiled project (Java)
[hint] ./DependencyApp.java:1:15
[hint] "gson is outdated, update to 2.11.0"
[hint]      gson 2.8.9 -> com.google.code.gson:gson:2.11.0
[hint] //> using dep com.google.code.gson:gson:2.8.9
[hint]               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Selected country: Germany

We can observe that Scala-CLI can detect the library version and suggest updates if a newer version is available. This feature is convenient for maintaining up-to-date dependencies without manually searching the Maven Central repository.

6. Packaging the Java Application as an Executable

One of Scala-CLI’s most valuable features is its ability to create executable applications directly from code without requiring additional plugins. Let’s explore creating an executable application using the previously created file, DependencyApp.java.

We can use the following command to create the executable:

scala-cli --power package DependencyApp.java -o myApp --assembly

The package command creates a package. The –power flag indicates that this is a power-user command. We can specify the executable app name using the argument -o. If we include the –assembly flag, Scala-CLI will create an executable JAR; otherwise, it creates a library JAR.

Scala-CLI creates an executable JAR by default when we use the assembly flag and wrap it in a standalone script. To run the created package, use the command:

./myApp

Instead of creating a wrapper executable, we can generate a simple JAR by passing an additional flag.

scala-cli --power package DependencyApp.java -o myApp.jar --assembly --preamble=false

Now, we can execute it as follows:

java -jar myApp.jar

Similarly, Scala-CLI allows us to create Docker images, GraalVM native images, and platform-specific formats such as deb, msi, etc.

7. Conclusion

In this article, we explored several key features of Scala-CLI, a tool similar to JBang. Scala-CLI, while strongly emphasizing Scala, also provides robust support for Java applications. We demonstrated how Scala-CLI streamlines development by simplifying various aspects of application building, offering easy customization, support for different packaging formats, and more.

As always, the sample code used in this tutorial is available over on GitHub.