{"id":1725,"date":"2021-01-11T20:17:26","date_gmt":"2021-01-11T20:17:26","guid":{"rendered":"https:\/\/jan.schnasse.org\/blog\/?p=1725"},"modified":"2026-09-28T11:38:32","modified_gmt":"2026-09-28T09:38:32","slug":"packaging-a-command-line-java-app-for-linux","status":"publish","type":"post","link":"https:\/\/jan.schnasse.org\/blog\/2021\/01\/11\/packaging-a-command-line-java-app-for-linux\/","title":{"rendered":"Packaging a Command Line Java App for Linux"},"content":{"rendered":"<p>How to create a java command line tool that is (1) easy to install (2) as small as possible (3) and does not interfere with a previously installed jvm on the host?<\/p>\n<h2>Here is my take<\/h2>\n<ol>\n<li>\u00a0Create an executable &#8218;fat jar&#8216;<\/li>\n<li>\u00a0Create a minimal jvm to run the fat jar<\/li>\n<li>\u00a0Define a proper version number<\/li>\n<li>\u00a0Package everything together to a <code>.deb<\/code> package<\/li>\n<li>\u00a0Provide the .deb package via an online repository<\/li>\n<\/ol>\n<p>All snippes were taken from <a href=\"https:\/\/github.com\/jschnasse\/oi\">https:\/\/github.com\/jschnasse\/oi\u00a0<\/a><\/p>\n<p>The oi command line app is a very simple conversion tool to transform structured formats from one into another.<\/p>\n<h2>Create an executable &#8218;fat jar&#8216;<\/h2>\n<p>I use the maven-assembly-plugin for this. Here is the relevant section from my <a href=\"https:\/\/github.com\/jschnasse\/oi\/blob\/master\/pom.xml\"><code>pom.xml<\/code>.<\/a><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"xml\">&lt;plugin&gt;\n        &lt;artifactId&gt;maven-assembly-plugin&lt;\/artifactId&gt;\n        &lt;executions&gt;\n          &lt;execution&gt;\n            &lt;phase&gt;package&lt;\/phase&gt;\n            &lt;goals&gt;\n              &lt;goal&gt;single&lt;\/goal&gt;\n            &lt;\/goals&gt;\n          &lt;\/execution&gt;\n        &lt;\/executions&gt;\n        &lt;configuration&gt;\n          &lt;finalName&gt;oi&lt;\/finalName&gt;\n          &lt;descriptorRefs&gt;\n            &lt;descriptorRef&gt;jar-with-dependencies&lt;\/descriptorRef&gt;\n          &lt;\/descriptorRefs&gt;\n          &lt;archive&gt;\n            &lt;manifest&gt;\n              &lt;mainClass&gt;org.schnasse.oi.main.Main&lt;\/mainClass&gt;\n            &lt;\/manifest&gt;\n            &lt;manifestEntries&gt;\n              &lt;Automatic-Module-Name&gt;org.schnasse.oi&lt;\/Automatic-Module-Name&gt;\n            &lt;\/manifestEntries&gt;\n          &lt;\/archive&gt;\n          &lt;appendAssemblyId&gt;false&lt;\/appendAssemblyId&gt;\n        &lt;\/configuration&gt;\n      &lt;\/plugin&gt;\n<\/pre>\n<p>The most important configuration entry is the path to the <code>&lt;mainClass&gt;<\/code> . The entry points to a java class that must define a main method.<\/p>\n<p>It also is important to define a fixed <code>&lt;finalName&gt;<\/code>. We don&#8217;t want to create artifacts with version numbers in it. The versioning is done elsewhere. Our build process should just spit out an executable at a predictable location.<\/p>\n<p>The <code>mvn package<\/code> command will now create a fat jar under <code>target\/oi.jar<\/code>.<\/p>\n<h2>Create a minimal jvm to run the &#8218;fat jar&#8216;<\/h2>\n<p>The created jar can be executed as <code>java -jar target\/oi.jar<\/code>. This is already an important milestone since you can now use the app on your own development pc. To make it a bit handier put the actual call into a script and copy it to <code>\/usr\/bin\/oi<\/code> in order to make it accessible for all users on the development machine. Also you can provide the oi.jar at a more global location, e.g. \/usr\/lib.<\/p>\n<p>This could be the content of \/usr\/bin\/oi<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"null\">java -jar \/usr\/lib\/oi.jar $@<\/pre>\n<p>Use <code>$@<\/code> to pass parameters from command line to the actual java app.<\/p>\n<p>More on this will be explained in the &#8218;Package everything together&#8216; section.<\/p>\n<p>The next step is to make the program executable on other machines. Since the application depends on the existence of the <code>java<\/code> interpreter we have to find a way to either ship <code>java<\/code>\u00a0 together with our little <code>oi<\/code> tool or to ask the user\/user&#8217;s computer to install it in advance.<\/p>\n<p>Both approaches are feasible. I decided to ship <code>java<\/code> together with my tool for the following reasons (1) The tool should be as self contained as possible (2) The installation of the tool should not interfere with other java based packages. (3) I want to be free to update to new jvm versions at my own speed, therefore I want\u00a0 support only one single jvm version at every state of development.<\/p>\n<p>Today <code>java<\/code> distributions come with a tool named <code>jlink<\/code>. The <code>jlink<\/code>tool can be used to create minimal jvms. This will look like:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">jlink \\\n    --add-modules java.base,java.naming,java.xml \\\n    --verbose \\\n    --strip-debug \\\n    --compress=1 \\\n    --no-header-files \\\n    --no-man-pages \\\n    --output \/opt\/jvm_for_oi<\/pre>\n<p>The result is a minimal jvm only containing the modules <code>java.base,java.naming,java.xml<\/code> under <code>\/opt\/jvm_for_oi<\/code>. The idea is now to provide this jvm together with our app. But to become a bit more independent from the configuration of my\u00a0 development machine I want to guarantee that my tool is served always with a well defined <code>jvm<\/code> version and not just with the version I have installed at my development machine. To create a well defined build environment I will use docker. With docker I can create a minimal jvm on the basis of a predefined openJDK version. And here is how it works.<\/p>\n<p>1. Based on the code above we can create a file named <a href=\"https:\/\/github.com\/jschnasse\/oi\/blob\/master\/Dockerfile.build\">Dockerfile.build<\/a> to create the jvm based on the openJdk-12.0.1_12.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"null\">FROM adoptopenjdk\/openjdk12:jdk-12.0.1_12\nRUN jlink \\\n    --add-modules java.base,java.naming,java.xml \\\n    --verbose \\\n    --strip-debug \\\n    --compress 2 \\\n    --no-header-files \\\n    --no-man-pages \\\n    --output \/opt\/jvm_for_oi\n<\/pre>\n<p>We will use this docker definition just to create the jvm and copy it to our development environment. The docker image can be deleted directly afterwards.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"null\">docker build -t adopt_jdk_image -f Dockerfile.build .\ndocker create --name adopt_jdk_container adopt_jdk_image\ndocker cp adopt_jdk_container:\/opt\/jvm_for_oi \/usr\/share\/jvm_for_oi\ndocker rm adopt_jdk_container<\/pre>\n<p>The resulting jvm can be found under \/usr\/share\/jvm_for_oi.<\/p>\n<p>This again is a very important milestone. You can now edit your <a href=\"https:\/\/github.com\/jschnasse\/oi\/blob\/master\/src\/main\/resources\/oi\">startscript at \/usr\/bin\/oi<\/a> and use the generated jvm instead of your preinstalled java version. This will make the execution of the app independent of the globally installed java version and therefor more reliable.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">\/usr\/share\/jvm_for_oi\/bin\/java -jar \/usr\/lib\/oi.jar $@<\/pre>\n<p>In my project configuration the inclusion of the minimal jvm increases the size of the<code> .deb<\/code> package by <code>~10MB<\/code>. On the target system the jvm takes <code>~45MB<\/code> extra space. In my former setup I configured openJDK-11 as dependency in the Debian package which consumes roughly <code>~80MB<\/code> of extra space if newly installed.<\/p>\n<h2>Define a proper version number<\/h2>\n<p>Since <code>oi<\/code> is a java app built with maven I use the typical semantic versioning scheme which consists of three numbers (1) a\u00a0 <em>major<\/em>, (2) a <em>minor<\/em>, (3 ) and a <em>patch<\/em> number divided by dots. Example given, a version of &#8218;0.1.4&#8216; reads as follows:<\/p>\n<p>0 &#8211; No major version. There is no stable version yet. Development is still at an early stage.<\/p>\n<p>1 &#8211; First minor version. This is software at an very early stage. Usually minor versions are compatible to the recent major release. Since no major version exists this software has no reliable behavior yet.<\/p>\n<p>4 &#8211; There were four patches released for the first minor version. A patch is typically a bug fix that does not change the<\/p>\n<p>The process of creating\u00a0 a new version is done as the following. (1) Define the next Version in a variable <code>oi_version<\/code> stored in a file <code>VERSIONS<\/code>. (2) Use a script <a href=\"https:\/\/github.com\/jschnasse\/oi\/blob\/master\/bumpVersion.sh\"><code>bumpVersions.sh<\/code><\/a> to\u00a0 update the version numbers in several files like README, manpage, etc. (3) Commit files that were updated with the new version number to git. (4) Use the mvn-gitflow plugin to create new versions for the actual source and to push everything in a well defined manner to github.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"xml\">&lt;plugin&gt;\n  &lt;groupId&gt;com.amashchenko.maven.plugin&lt;\/groupId&gt;\n  &lt;artifactId&gt;gitflow-maven-plugin&lt;\/artifactId&gt;\n  &lt;version&gt;1.7.0&lt;\/version&gt;\n  &lt;configuration&gt;\n    &lt;gitFlowConfig&gt;\n        &lt;developmentBranch&gt;master&lt;\/developmentBranch&gt;\n    &lt;\/gitFlowConfig&gt;\n  &lt;\/configuration&gt;\n&lt;\/plugin&gt;<\/pre>\n<p>The gitflow-maven-plugin supports the command <code>mvn gitflow:release<\/code> . The command does the following:<\/p>\n<p>1. Define a new release number<\/p>\n<p>2. Update the pom.xml in the development branch accordingly<\/p>\n<p>3. Push the updated pom.xml to the mainline branch<\/p>\n<p>4. Create a tag on mainline<\/p>\n<p>5. Update the release number in the development branch to a new SNAPSHOT release.<\/p>\n<p>6. Push the updated pom.xml to the development branch.<\/p>\n<p>The plugin was originally created with for the `gitflow` branching approach. Since my project uses the <code>github-flow<\/code>-branching approach which does not foresee a development branch besides of the mainline I defined master as development branch.<\/p>\n<h2>Package everything together<\/h2>\n<p>At this point a new release of the sourcecode is online at github. Now, it&#8217;s time to create the binary release. The binary release will be a .deb file containing the newly packaged fat-jar together with the minimal jvm. (5) A <a href=\"https:\/\/github.com\/jschnasse\/oi\/blob\/master\/build.sh\">build.sh<\/a> script is used to create the .deb artifact.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"null\">#!\u00a0\/bin\/bash\n\nscriptdir=\"$(\u00a0cd\u00a0\"$(\u00a0dirname\u00a0\"${BASH_SOURCE[0]}\"\u00a0)\"\u00a0&amp;&amp;\u00a0pwd\u00a0)\"\ncd\u00a0$scriptdir\nsource\u00a0VERSIONS\nmvnparam=$1\n\nfunction build_oi(){\n package_name=$1\n package_version=$2\n package=${package_name}_$package_version\n mkdir -p deb\/$package\/usr\/lib\n mkdir -p deb\/$package\/usr\/bin\n mkdir -p deb\/$package\/usr\/share\/man\/man1\/\n mvn package -D$mvnparam\n sudo cp src\/main\/resources\/$package_name deb\/$package\/usr\/bin\n sudo cp target\/$package_name.jar deb\/$package\/usr\/lib\n\ndocker build -t adopt_jdk_image -f Dockerfile.build .\ndocker create --name adopt_jdk_container adopt_jdk_image\ndocker cp adopt_jdk_container:\/opt\/jvm_for_oi deb\/$package\/usr\/share\/jvm_for_oi\ndocker rm adopt_jdk_container\n\nln -s ..\/share\/jvm_for_oi\/bin\/java deb\/$package\/usr\/bin\/jvm_for_oi \n\n}\n\nfunction build(){\n package_name=$1\n package_version=$2\n package=${package_name}_$package_version\n\n if [ -d $scriptdir\/man\/$package_name ]\n then\n   cd $scriptdir\/man\/$package_name\n   asciidoctor -b manpage man.adoc\n   cd -\n   sudo cp $scriptdir\/man\/$package_name\/$package_name.1 deb\/$package\/usr\/share\/man\/man1\/\n fi  \n dpkg-deb --build deb\/$package\n}\n\nbuild_oi oi $oi_version<\/pre>\n<p>What you can see from the listing is that the script creates a directory structure in accordance to the .deb package format. It also generates (1) the fat-jar, (2) the minimal jvm (3) a man page and (4) binds it all together with a <code>dpkg-deb -build<\/code> command<\/p>\n<h2>Provide the .deb package via an online repository<\/h2>\n<p>(6) The .deb artifact is then uploaded to a bintray repo using again a shell script <code><code><\/code><\/code><a href=\"https:\/\/github.com\/jschnasse\/oi\/blob\/master\/push_to_bintray.sh\">push_to_bintray.sh<\/a>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"null\">#! \/bin\/bash\n\nscriptdir=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" &amp;&amp; pwd )\"\nsource VERSIONS\n\nfunction push_to_bintray(){\ncd $scriptdir\nPACKAGE=$1\nVERSION=$2\nAPI_AUTH=$3\nsubject=jschnasse\nrepo=debian\nfilepath=${PACKAGE}_${VERSION}.deb\ncurl -u$API_AUTH -XPOST \"https:\/\/bintray.com\/api\/v1\/packages\/$subject\/$repo\/\" -d@bintray\/${PACKAGE}\/package.json -H\"content-type:application\/json\"\ncurl -u$API_AUTH -XPOST \"https:\/\/bintray.com\/api\/v1\/packages\/$subject\/$repo\/$PACKAGE\/versions\" -d@bintray\/${PACKAGE}\/version.json -H\"content-type:application\/json\"\ncurl -u$API_AUTH -T deb\/$filepath \"https:\/\/bintray.com\/api\/v1\/content\/$subject\/$repo\/$PACKAGE\/$VERSION\/$filepath;deb_distribution=buster;deb_component=main;deb_architecture=all;publish=1;override=1;\"\ncurl -u$API_AUTH -XPUT \"https:\/\/bintray.com\/api\/ui\/artifact\/$subject\/$repo\/$filepath\" -d'{\"list_in_downloads\":true}' -H\"content-type:application\/json\"\ncd -\n}\napiauth=$1\npush_to_bintray oi $oi_version $apiauth\npush_to_bintray lscsv $lscsv_version $apiauth\npush_to_bintray libprocname $libprocname_version $apiauth<\/pre>\n<p>The script makes use <a href=\"https:\/\/github.com\/jschnasse\/oi\/tree\/master\/bintray\/oi\">of a set of prepared json files<\/a> to provide metadata for the\u00a0 package.<\/p>\n<p>(7) The last step is now to visit the github wegpage an navigate to the tag that has been created at step (4). By adding a release name it will become visible as release at the landing page of the git repo.<\/p>\n<p>Step 6 seems the most critical step since it updates the debian repo and makes the new version available to everyone. In between step 5 and step 6 some sort of testing should happen to ensure that the artifact is installable and does execute as expected. My plan is to utilize a set of docker files to test releases. A first attempt can be found <a href=\"https:\/\/github.com\/jschnasse\/oi\/tree\/master\/docker\">here<\/a>.<\/p>\n<h2>Fazit<\/h2>\n<p>The process of versioning consists of multiple steps. Most of the work can be automated. A semi automated process can be developed with little effort. To automate the whole process it is crucial to provide well thought tests in between the steps and to define fallback points. This adds some extra safety to the objective but also introduces extra complexity. For future jdk versions it could be beneficial to use <code>jpackager<\/code> instead of <code>jlink<\/code>.<\/p>\n<p>&nbsp;<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>How to create a java command line tool that is (1) easy to install (2) as small as possible (3) and does not interfere with a previously installed jvm on the host? Here is my take \u00a0Create an executable &#8218;fat jar&#8216; \u00a0Create a minimal jvm to run the fat jar \u00a0Define a proper version number [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,6,9],"tags":[],"class_list":["post-1725","post","type-post","status-publish","format-standard","hentry","category-admin","category-development","category-java"],"_links":{"self":[{"href":"https:\/\/jan.schnasse.org\/blog\/wp-json\/wp\/v2\/posts\/1725","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jan.schnasse.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/jan.schnasse.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/jan.schnasse.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/jan.schnasse.org\/blog\/wp-json\/wp\/v2\/comments?post=1725"}],"version-history":[{"count":1,"href":"https:\/\/jan.schnasse.org\/blog\/wp-json\/wp\/v2\/posts\/1725\/revisions"}],"predecessor-version":[{"id":4022,"href":"https:\/\/jan.schnasse.org\/blog\/wp-json\/wp\/v2\/posts\/1725\/revisions\/4022"}],"wp:attachment":[{"href":"https:\/\/jan.schnasse.org\/blog\/wp-json\/wp\/v2\/media?parent=1725"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/jan.schnasse.org\/blog\/wp-json\/wp\/v2\/categories?post=1725"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/jan.schnasse.org\/blog\/wp-json\/wp\/v2\/tags?post=1725"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}