JavaRush/Courses/Kotlin SELF/Building a JAR: artifact, entry point, and running outsid...

Building a JAR: artifact, entry point, and running outside the IDE

Open

1. What is an artifact, and why a JAR is a “parcel” with your program

When you write code, it can feel like “the program is my .kt files.” But as soon as you hit Run or start a build, the magic of engineering bureaucracy happens: source code turns into a build result that you can hand to another person, a server, or even yourself a week later—when you’ve forgotten where the Run button was.

That build result is called an artifact. On the JVM, the most common artifact is a JAR file. It’s convenient to think of it as a parcel: inside are compiled .class files (and sometimes resources), plus service metadata. The idea is simple: you don’t carry source code and a compiler around; you deliver a ready-to-use “packaged result.”

To avoid confusion, keep this simple table in mind:

Term In plain words Example
Source code What you write
src/main/kotlin/app/Main.kt
Build The process of turning source code into a result
./gradlew build
Artifact What you get after the build
build/libs/app-1.0.0.jar
JAR An artifact format for the JVM One .jar file

And the important thought of the day: IDEs often hide the details, while a JAR forces you to see the world as it really is. Sometimes that’s useful. Sometimes it’s suddenly painful—but still useful.

2. How Kotlin turns into something the JVM can run

Now for a bit of “behind the scenes”—just enough to understand JAR files and the entry point, but not enough for us to disappear into the dark woods of bytecode and JVM shamanism. Kotlin/JVM compiles into Java-compatible .class files. And those .class files are what the JVM actually runs.

A Kotlin quirk that matters today: top-level functions (functions outside classes) still have to “land” somewhere in the JVM world, where everything revolves around classes. So Kotlin puts such functions into a special class whose name is usually derived from the file name.

For example, if you have a file Main.kt and it contains:

package app

fun main() {
    println("Hello from JAR") // Hello from JAR
}

then at the JVM level it will look as if there is a class app.MainKt with a main method. From Kotlin’s point of view, this is almost invisible—until you start configuring the build and running a JAR, where you need to specify “which main exactly should be launched?”

Remember this as a practical rule (without trying to memorize everything forever): if your main is in Main.kt in the app package, then the JVM entry point name will usually be app.MainKt.

3. Entry point: how a JAR knows which main to run

When you run a program from an IDE, it usually figures out which main you selected (because you clicked the green button right next to it). But when you run a JAR outside the IDE, there’s no “green button,” and the JVM needs an instruction: which class should be considered the startup class.

There are two most basic ways to run:

  1. java -cp ... <MainClass> — you explicitly tell the JVM which class is the startup class.
  2. java -jar app.jar — you’re saying: “the JAR already says what to start.”

The second way works only if the JAR is runnable. What makes it runnable is a manifest file that contains an entry for the main class. Inside the JAR there is a service file:

META-INF/MANIFEST.MF

And it may contain a line (simplified):

Main-Class: app.MainKt

You can think of it as a label on the box: “Open here.”

Let’s draw a simple diagram so it doesn’t feel like “magic”:

flowchart TD
    A[Your .kt files] --> B[Kotlin/JVM compilation]
    B --> C[.class files]
    C --> D[JAR packaging]
    D --> E["JAR + META-INF/MANIFEST.MF"]
    E --> F["java -jar ... starts Main-Class"]

Minimal Gradle setup: where you specify the main class

For today we won’t dive into how Gradle works internally, but it’s useful to see what this looks like in build.gradle.kts so it doesn’t feel scary.

The clearest “intent marker” is to apply the application plugin and set mainClass. Example (fragment):

plugins {
    kotlin("jvm") version "2.3.0"
    application
}

application {
    mainClass.set("app.MainKt")
}

This reads almost like plain English: “this is an application, and the entry point is this one.” Under the hood, Gradle will help both with running and with generating metadata.

If your project has multiple main functions, this is where you “declare the primary one.” That immediately reduces chaos: you stop guessing which main was running yesterday.

4. Where to find the JAR after a build

When you’re just learning, it seems like the program lives in src. When you’re a bit more experienced, you realize: the program lives in the artifact, and src is the recipe. By default, Gradle puts build results into the build directory, and the JAR is most often located in:

build/libs/

That’s exactly where you should look if you want to see what actually runs outside the IDE.

So this section doesn’t turn into a collection of spells, keep one table: “what I do — what I get.”

Action What Gradle does What you expect to see
./gradlew build
Builds the whole project (including checks, if configured) Results appear in the
build/
folder
./gradlew jar
Builds specifically the JAR package A
.jar
appears in
build/libs/
./gradlew run
(if
application
is enabled)
Runs the application via Gradle Console output as with a normal run

On Windows, instead of ./gradlew you typically use gradlew.bat, but the idea is the same: you ask Gradle to perform actions the IDE used to do “quietly in the background.”

5. Running outside the IDE: why args, env, and the working directory matter

At this point many people have their first “grown-up” encounter with reality: “it works in the IDE, but not in the terminal.” And that’s not because Kotlin is offended. It’s just that the IDE injects a lot of settings at launch time: working directory, arguments, environment variables, sometimes even console encoding. Outside the IDE you get a “clean run,” where everything depends on how exactly you started the command.

To make this as concrete as possible, let’s continue with our educational console app. Let it be called CliBox (a box of commands—sounds like something you could later sell if you add the word “AI”).

Environment diagnostics: printing args, env, and working directory

We’ll make a small function you can call at the start of main. It doesn’t do anything “smart”—it just prints facts. But functions like this save hours of debugging.

package app

fun printRunDiagnostics(args: Array<String>) {
    val dir = System.getProperty("user.dir")
    val mode = System.getenv("APP_MODE") ?: "not set"

    println("Args count = ${args.size}")       // Args count = ...
    println("APP_MODE = $mode")                // APP_MODE = ...
    println("Working dir = $dir")              // Working dir = ...
}

Note: System.getenv("APP_MODE") returns a nullable value, and we immediately apply ?:. That’s exactly the skill that already helped you in the null-safety topic.

Now let’s plug it into main:

package app

fun main(args: Array<String>) {
    printRunDiagnostics(args)
    println("CliBox started") // CliBox started
}

Command-line arguments: don’t assume they exist

A very common mistake when running outside the IDE is expecting that arguments will “somehow appear.” But if you didn’t pass any arguments, args.size == 0, and args[0] will crash.

Let’s make a hello command that takes a name from args:

package app

fun main(args: Array<String>) {
    val name = if (args.size >= 2) args[1] else "stranger"
    val cmd = if (args.isNotEmpty()) args[0] else "help"

    if (cmd == "hello") println("Hello, $name!") else println("Use: hello <name>")
    // Hello, Alice!  (example)
}

The format here is simple: the first argument is the command, the second is the parameter. This isn’t the most powerful CLI, but it’s perfect for understanding what happens when you run a JAR.

6. JAR in real life: library, application, and a “manual run-through”

It’s important not to fall into the trap “if it’s a JAR, it must be runnable.” A JAR is a container, and it can be either a library or an application. A library may have no entry point at all: you add it to another project as a dependency, and running it “by itself” isn’t expected.

An application, on the other hand, has the point of “run it and get behavior.” Then you need a main and usually you need Main-Class in the manifest (if you want to run via java -jar).

Sometimes it’s useful to know a fallback way to run without a runnable manifest. It looks like this:

java -cp app.jar app.MainKt

This reads: “put app.jar on the classpath and start the app.MainKt class.” But for a beginner it’s better to aim for a clear runnable scenario so launching stays short and predictable.

Mini run-through: from build to launch

Now let’s assemble the full path in our heads. Imagine you want to send your app to a friend (or to a server), and there’s no IDE there. Then you usually do two parts: build the artifact and run it.

Build in the terminal from the project root (where build.gradle.kts is located):

./gradlew build

After that, check the folder:

ls build/libs

And find the JAR (the name depends on version and the project name), for example:

app-1.0.0.jar

Run:

java -jar build/libs/app-1.0.0.jar hello Alice

And expect to see something like:

Hello, Alice!

If you also enabled diagnostics in main, you’ll see the arguments, the working directory, and the APP_MODE value.

Environment variables: “tweaking behavior” without rewriting code

In real life, programs often take configuration from env variables because it’s convenient for running in different environments. Even for the educational CliBox you can add logic: if APP_MODE=dev, then print extra text.

package app

fun isDevMode(): Boolean {
    val mode = System.getenv("APP_MODE") ?: return false
    return mode.lowercase() == "dev"
}

And use it:

package app

fun main(args: Array<String>) {
    if (isDevMode()) println("Dev mode is ON") // Dev mode is ON
    println("CliBox started")                  // CliBox started
}

Then at launch (roughly; the syntax depends on the shell):

APP_MODE=dev java -jar build/libs/app-1.0.0.jar

The program changes behavior without changing the source code—only due to the environment. That’s exactly why running outside the IDE is so important: you start seeing that a program is not just code, but also the run conditions.

Why “it works in the IDE, but not in the JAR”: how to think so you don’t panic

When a program breaks outside the IDE, your brain usually offers two paths: “I broke everything” and “the computer broke.” There’s a calmer third path: compare the launch conditions.

In the IDE you might have had arguments configured that you forgot to pass to java -jar. You might have had the working directory set to the project root, but in the terminal you ran the JAR from a different folder. You might have set an environment variable in the Run configuration, but in a normal console it isn’t set.

So a useful habit is: before you fix code, print the facts. That’s exactly why we wrote printRunDiagnostics. It’s a small “flashlight”: it doesn’t fix the problem, but it helps you avoid wandering in the dark yelling “where am I?”

7. Typical mistakes when building and running a JAR

Mistake #1: thinking “any JAR can be run with java -jar”.
The JAR format is the same, but the meaning can be different. If the JAR is a library, it may have no entry point, and that’s normal. Even if you have a main in sources, the JAR may be built without the required Main-Class in the manifest, and then java -jar won’t know what to start. In such cases you either configure a runnable JAR or run via -cp while specifying the main class.

Mistake #2: multiple main functions in the project and “guessing” which one runs.
Early on it’s easy to create a second main “for an experiment,” then a third “for a test,” and then wonder why Gradle is running the wrong one. The IDE usually saves you here because you click a specific main, but building a JAR wants unambiguity. It’s better to keep one primary main for the application and not turn the project into an amusement park.

Mistake #3: accessing args[0] without checking the size.
In the IDE you might always run the program with arguments (because you configured the Run Configuration once), but in the terminal you forgot to pass them. The result is an array index out of bounds error. The cure is simple discipline: first check args.isNotEmpty() or args.size, and only then read by index.

Mistake #4: expecting an env variable to be “definitely set.”
System.getenv("X") returns String?, and that’s an honest contract: the variable might not exist. In the IDE you might have set it in the run settings, but in the terminal it’s empty. If the code isn’t ready for null, you’ll get either incorrect behavior, an abnormal termination, or “weird defaults.” The Elvis operator ?: and careful checks are your friends here.

Mistake #5: not understanding the working directory and relative paths (even if you’re not working with files yet).
Even if you aren’t reading files yet, the working directory affects a lot: where you start the process, which relative paths will “count,” where you expect resources from. In an IDE, the working directory is often set to the project root automatically, but in a terminal you might be in a completely different place. That’s why printing System.getProperty("user.dir") is unexpectedly useful diagnostics.

Mistake #6: trying to “fix” Gradle without understanding that you’re fixing config, not the application.
When the build fails, beginners sometimes start adding lines to build.gradle.kts almost like a magic scroll. A better strategy is gentler: remember that build.gradle.kts is build configuration; change it in small steps; watch brackets and quotes; and after changes, refresh the project model (sync), otherwise the IDE and Gradle may end up living in “different realities.”

1
Task
Kotlin SELF,  17 level,  4 lecture
Locked
Greeting command
Greeting command
1
Task
Kotlin SELF,  17 level,  4 lecture
Locked
Startup diagnostics
Startup diagnostics
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet