Friday, January 13, 2017

Today's Rug: maven executable jar

I like being a polyglot developer, even though it's painful sometimes. I use lots of languages, and in every one I have to look stuff up. That costs me time and concentration.

Yesterday I wanted to promote my locally-useful project from "I can run it in the IDE" to "I can run it at the command line." It's a Scala project built in maven, so I need an executable jar. I've looked this up and figured this out at least twice before. There's a maven plugin you have to add, and then I have to remember how to run an executable jar, and put that in a script. All this feels like busywork.

What's more satisfying than cut-and-pasting into my pom.xml and writing another script? Automating these! So I wrote a Rug editor. Rug editors are code that changes code. There's a Pom type in Rug already, with a method for adding a build plugin, so I cut and paste the example from the internet into my Rug. Then I fill in the main class; that's the only thing that changes from project to project so it's a parameter to my editor. Then I make a script that calls the jar. (The script isn't executable. I submitted an issue in Rug to add that function.) The editor prints out little instructions for me, too.

$ rug edit -lC ~/code/scala/org-dep-graph MakeExecutableJar main_class=com.jessitron.jessMakesAPicture.MakeAPicture

Resolving dependencies for jessitron:scattered-rugs:0.1.0 ← local completed
Loading jessitron:scattered-rugs:0.1.0 ← local into runtime completed
run `mvn package` to create an executable jar
Find a run script in your project's bin directory. You'll have to make it executable yourself, sorry.
Running editor MakeExecutableJar of jessitron:scattered-rugs:0.1.0 ← local completed

→ Project
  ~/code/scala/org-dep-graph/ (8 mb in 252 files)

→ Changes
  ├── pom.xml updated 2 kb
  ├── pom.xml updated 2 kb
  ├── bin/run created 570 bytes
  └── .atomist.yml created 702 bytes

Successfully edited project org-dep-graph

It took a few iterations to get it working, probably half an hour more than doing the task manually.
It feels better to do something permanently than to do it again.

Encoded in this editor is knowledge:
* what is that maven plugin that makes an executable jar? [1]
* how do I add it to the pom? [2]
* what's the maven command to build it? [3]
* how do I get it to name the jar something consistent? [4]
* how do I run an executable jar? [5]
* how do I find the jar in a relative directory from the script? [6]
* how do I get that right even when I call the script from a symlink? [7]

It's like saving my work, except it's saving the work instead of the results of the work. This is going to make my brain scale to more languages and build tools.

below the fold: the annotated editor. source here, instructions here in case you want to use it -> or better, change it -> or even better, make your own.

@description "teaches a maven project how to make an executablejar"
@tag "maven"
editor MakeExecutableJar

@displayName "Main Class"
@description "Fully qualified Java classname"
@minLength 1
@maxLength 100
param main_class: ^.*$

let pluginContents = """<plugin>
[4]         </configuration>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
""" [2]

let runScript = """#!/bin/bash

while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
  SOURCE="$(readlink "$SOURCE")"
  [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done [7]
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"

java -jar $DIR/../target/executable.jar "$@" [5]

with Pom p
  do addOrReplaceBuildPlugin "org.apache.maven.plugins" "maven-shade-plugin" pluginContents [1]

with File f when path = "pom.xml" begin
  do replace "__I_AM_THE_MAIN__" main_class
  do eval { print("run `mvn package` to create an executable jar")[3]

with Project p begin
  do eval { print("Find a run script in your project's bin directory. You'll have to make it executable yourself, sorry") }
  do addFile "bin/run" runScript

No comments:

Post a Comment