CS 346 (W23)
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Packaging

Applications

Packaging just refers to putting together your software components in a way that you can distribute to your end-user. As we’ve seen, this could be physical (a retail box including a DVD), or a digital image (ISO image, package file, installer executable). It’s the “bundling” part of building software.

Packaging is also one of those tasks that we tend to hand wave: just compile, link and hand-out the executable right? Unfortunately it’s not that simple. Apart from the executable, your application may also include images, sound clips and other resources, plus external libraries that you don’t necessarily want to statically link into your executable. You need to distribute all of these files, and ensure that they are installed in the correct location, registered with the OS and made usable by your application. You also need to perform steps to ensure that your application respects conventions of the particular environment: installing under Program Files in Windows, putting libraries under Windows\\System32, putting an icon on the desktop and so on.

The way we address the compexities of packaging is to break down the process into multiple steps, which are handled by different tools:

Step Explanation
Step 1: Compiling classes Use the kotlinc compiler to create classes from our source code.
Step 2: Creating archives Use the jar command to create jar files of classes and related libraries.
Step 3: Creating scripts Optionally, create scripts to allow a user to execute directly from the JAR files.

In the next sections, we’ll demonstrate using each of these tools.

1. Compiling classes

Let’s use the HelloWorld sample to demonstrate different methods for preparing, and then packaging our application.

fun main(args: Array<String>) {
	println(Hello Kotlin!)
}

To compile from the command-line, we can use the Kotlin compiler, kotlinc. By default, it takes Kotlin source files (.kt) and compiles them into corresponding class files (.class) that can be executed on the JVM.

$ kotlinc Hello.kt 

$ ls 
Hello.kt HelloKt.class

$ kotlin HelloKt
Hello Kotlin!

We could also just do this with IntelliJ IDEA or Android Studio, where Gradle - build - build will generate class files. As we will see at the end of this section, we can often just generate the final installer in a single step without doing each step manually.

2. Creating archives

Class files by themselves are difficult to distribute. The Java platform includes the jar utility, used to create a single archive file from your classes.

A JAR file is a standard mechanism in the Java ecosystem for distributing applications. It’s effectively just a compressed file (just like a ZIP file) which has a specific structure and contents. Most distribution mechanisms expect us to create a JAR file first.

This example compiles Hello.kt, and packages the output in a Hello.jar file.

$ kotlinc Hello.kt -include-runtime -d Hello.jar

$ ls
Hello.jar Hello.kt

The -d option tells the compiler to package all of the required classes into our jar file. The -include-runtime flag tells it to also include the Kotlin runtime classes. These classes are needed for all Kotlin applications, and they’re small, so you should always include them in your distribution (if you fail to include them, you app won’t run unless your user has Kotlin installed).

To run from a jar file, use the java command1.

$ java -jar Hello.jar
Hello Kotlin!

Our JAR file from above looks like this if you uncompress it:

$ unzip Hello.jar -d contents
Archive:  Hello.jar
  inflating: contents/META-INF/MANIFEST.MF  
  inflating: contents/HelloKt.class  
  inflating: contents/META-INF/main.kotlin_module  
  inflating: contents/kotlin/collections/ArraysUtilJVM.class 
	...

$ tree -L 2 contents/
.
├── META-INF
│   ├── MANIFEST.MF
│   ├── main.kotlin_module
│   └── versions
└── kotlin
    ├── ArrayIntrinsicsKt.class
    ├── BuilderInference.class
    ├── DeepRecursiveFunction.class
    ├── DeepRecursiveKt.class
    ├── DeepRecursiveScope.class
    ...

The JAR file contains these main features:

  • HelloKt.class – a class wrapper generated by the compiler
  • META-INF/MANIFEST.MF – a file containing metadata.
  • kotlin/ – Kotlin runtime classes not included in the JDK.

The MANIFEST.MF file is autogenerated by the compiler, and included in the JAR file. It tells the runtime which main method to execute. e.g. HelloKt.main().

$ cat contents/META-INF/MANIFEST.MF 
Manifest-Version: 1.0
Created-By: JetBrains Kotlin
Main-Class: HelloKt

In IntelliJIDEA, Gradle - build - jar will create a jar file. As we will see at the end of this section, we can often just generate the final installer in a single step without doing each step manually.

3. Creating scripts

We can distribute JAR files like this to our users, but they’re awkward: users would need to have the Java JDK installed, and typejava -jar filename.jar to actually run our programs. For some scenarios this might be acceptable (e.g. launching a service from an init script), but most of the time we want a more user-friendly installation mechanism.

The simplest thing we can do it create a script to launch our application from a JAR file, with the same effect as executing from the command-line:

$ cat hello
#!/bin/bash 
java -jar hello.jar

$ chmod +x hello

$ ./hello
Hello Kotlin!

For simple applications, especially ones that we use ourselves, this may be sufficient. It has the downside of requiring the user to have the Java JDK installed on their system2. This particular script is also not very robust: it doesn’t set runtime parameters, and doesn’t handle more than a single JAR file.

For a more robust script, we can let Gradle generate one for us. In IntelliJIDEA, Gradle - distribution - distZip will create a zip file that includes a custom runtime script. Unzip it, and it will contain a lib directory with all of the JAR files, and a bin directory with a script that will run the application from the lib directory. Copy both to a new location and you can execute the app from there.

Here’s an example of the Archey project, with a distZip file that was generated by Gradle.

$ tree build/distributions -L 3
build/distributions
├── app
│   ├── bin
│   │   ├── app
│   │   └── app.bat
│   └── lib
│       ├── annotations-13.0.jar
│       ├── app.jar
│       ├── checker-qual-3.8.0.jar
│       ├── error_prone_annotations-2.5.1.jar
│       ├── failureaccess-1.0.1.jar
│       ├── guava-30.1.1-jre.jar
│       ├── j2objc-annotations-1.3.jar
│       ├── jsr305-3.0.2.jar
│       ├── kotlin-stdlib-1.5.31.jar
│       ├── kotlin-stdlib-common-1.5.31.jar
│       ├── kotlin-stdlib-jdk7-1.5.31.jar
│       ├── kotlin-stdlib-jdk8-1.5.31.jar
│       └── listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar
└── app.zip

The bin/app script is quite robust, and will handle many different types of system configurations. It will also handle setting the CLASSPATH for all of the libraries that are included in the lib directory!

Here’s a portion of the app script. You don’t want to have to write this by-hand!

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in                #(
  CYGWIN* )         cygwin=true  ;; #(
  Darwin* )         darwin=true  ;; #(
  MSYS* | MINGW* )  msys=true    ;; #(
  NONSTOP* )        nonstop=true ;;
esac

CLASSPATH=$APP_HOME/lib/app.jar:$APP_HOME/lib/kotlin-stdlib-jdk8-1.5.31.jar:$APP_HOME/lib/guava-30.1.1-jre.jar:$APP_HOME/lib/kotlin-stdlib-jdk7-1.5.31.jar:$APP_HOME/lib/kotlin-stdlib-1.5.31.jar:$APP_HOME/lib/kotlin-stdlib-common-1.5.31.jar:$APP_HOME/lib/failureaccess-1.0.1.jar:$APP_HOME/lib/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar:$APP_HOME/lib/jsr305-3.0.2.jar:$APP_HOME/lib/checker-qual-3.8.0.jar:$APP_HOME/lib/error_prone_annotations-2.5.1.jar:$APP_HOME/lib/j2objc-annotations-1.3.jar:$APP_HOME/lib/annotations-13.0.jar


# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD=$JAVA_HOME/jre/sh/java
    else
        JAVACMD=$JAVA_HOME/bin/java
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

The script works well. Here’s archey being executed from the script:

$ build/distributions/app/bin/app 

               ####                   User: jaffe
               ###                    Home: /Users/jaffe
       #######    #######             Name: Bishop.local
     ######################           OS: Mac OS X
    #####################             Version: 11.3
    ####################              CPU: x86_64
    ####################              Cores: 10
    #####################             Free Memory: 514 MB
     ######################           Total Memory: 520 MB
      ####################            Disk Size: 582 GB
        ################              Free Space: 582 GB
         ####     #####               IP Address: 127.0.0.1

However, the script doesn’t work so well when we require more complex runtime components. For example, using the script for a JavaFX application produces an error:

$ ./clock_advanced
Error: JavaFX runtime components are missing, and are required to run this application

We’ll address GUI applications in that specific section.

Resources


  1. In the same way that the Java compiler compiles Java code into IR code that will run on the JVM, the Kotlin compiler also compiles Kotlin code into compatible IR code. The java command will execute any IR code, regardless of which programming language and compiler was used to produce it. ↩︎

  2. When Java was first introduced, the assumption was that every platform would have the JVM pre-installed, and we could just use that. Unfortunately, we often needed a specific version of the JVM, so companies ended up distributing and installing their own JVM alongside their applications. Not a great situation. ↩ ↩︎