From 3fe296294d00af43a148cb6ce83193805fd9ec56 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Tue, 11 Dec 2018 18:58:55 -0800 Subject: Initial commit --- .gitignore | 1 + Makefile | 53 +++++++++++++++++++++++++++++ Maven.list | 6 ++++ README.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++ scripts/combinejars | 26 ++++++++++++++ src/main.scala | 6 ++++ 6 files changed, 190 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 Maven.list create mode 100644 README.md create mode 100755 scripts/combinejars create mode 100644 src/main.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f97022 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8a38ee4 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +sources=$(wildcard src/*.scala) +main=Main + +all: target/library.jar + +# contains jars of all dependencies (including transitive ones) +target/dependencies.list: Maven.list + mkdir -p target + coursier fetch $(shell sed '/^#/d' Maven.list) > $@ + +# concatenate dependencies to be parseable as a classpath argument +target/classpath.line: target/dependencies.list + paste --serial --delimiters ':' $^ > $@ + +# compile scala source files (note that a stamp file is used, since +# many classfiles may be created per source file) +target/classfiles.stamp: $(sources) target/classpath.line + mkdir -p target/classfiles + scalac \ + -d target/classfiles \ + -cp $(shell cat target/classpath.line) \ + -target:jvm-1.8 \ + $(sources) + touch target/classfiles.stamp + +# bundle classfiles into a jar +target/library.jar: target/classfiles.stamp + jar cf $@ -C target/classfiles . + +# combine lirary jar with all dependencies to produce a fat jar +target/application.jar: target/library.jar target/dependencies.list + mkdir -p target + scripts/combinejars $@ $(shell cat target/dependencies.list) $< + +# inject a startup script to the fat jar, producing an executable +target/application: target/application.jar + echo "#!/bin/sh" > target/application + echo 'exec java -cp $$0 $(main) "$$@"' >> target/application + cat target/application.jar >> target/application + chmod +x target/application + +# run the application +run: target/application.jar + @java -cp $< $(main) + +# format source files +fmt: + scalafmt $(sources) + +clean: + rm -rf target + +.PHONY: clean all diff --git a/Maven.list b/Maven.list new file mode 100644 index 0000000..6114b60 --- /dev/null +++ b/Maven.list @@ -0,0 +1,6 @@ +# Maven coordinates of dependencies are listed here. +# +# lines starting with # are ignored +# +io.crashbox:identicon_2.12:0.2.0 +#com.typesafe.akka:akka-actor_2.12:2.5.19 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c052c12 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# A makefile for Scala Applications + +A makefile that demonstrates how building Scala applications can be +very simple. + +It supports *dependency resolution of maven artefacts*, enables +building *executables* (not only fat jars, actual executable files) +and all **builds are reproducible** (anyone building the same source +gets bit-wise identical binaries). + +## Dependencies + +As this is a plain makefile, it relies on a number of tools that must +be present in the caller's environment. Those are: + +- [scalac](https://www.scala-lang.org/download/) to compile Scala +- [coursier](https://coursier.github.io/coursier/1.1.0-SNAPSHOT/docs/quick-start-cli.html#installation) for dependency resolution +- openjdk to run binaries and for packaging jar files +- zip and unzip for creating standalone fat jars +- (optional) [scalafmt](https://scalameta.org/scalafmt/docs/installation.html#cli) + +Tested on Linux, should work on Darwin and BSDs, does not support +Windows. + +## How-To + +Source files are in `src/` and maven dependencies are declared in +`Maven.list`. + +The main targets for a development workflow are typically + +- `make` to build a library jar in target/library.jar +- `make run` to run the application + +The application may be deployed as a standalone binary, only requiring +a Java runtime environemnt, by running `make target/application`. + +## Example + +This project contains an example application that prints the identicon +of a given string. Build it by running `make target/application`. + +- Generate an identicon: `target/application username`. +- View an identicon: `inkview <(target/application admin)`. + +## Known Issues / FAQ + +- **It's slow**. Yes, this Makefile invokes a cold compiler on every + source change. Using [bloop](https://scalacenter.github.io/bloop/) + could help here. + +- **Tools not included**. To run this makefile, all its constituent + tools (scalac, scalafmt, coursier, etc) must be configured in the + developer's environment. It's not as convenient for newcomers as the + out-of-the-box experience that sbt or cbt offer, however adhering to + the unix philosophy of "do one thing and do it well" is the whole + purpose of this project. + +- **Doesn't work with multiple Akka libraries**. The jar combination + script is quite dumb: it simply extracts contents of jar files in a + directory tree and recombines the final directory into a jar. In the + process, any identically named files get overwritten. Since Akka + relies on `reference.conf` files within jars, if multiple Akka + libraries are used, the latest config in the classpath overwrites + all others. A smarter merging script would be required to fix tis + issue. + +- **The resulting binary is large**. The fat jar which is contained in + the application contains *all* dependent jars, which may add up for + projects with complex transitive dependencies. Using a tool like + proguard could drastically decrease the size of the final binary. + +- **The resulting binary requires a Java runtime**. See + [scala-native](http://www.scala-native.org/en/v0.3.8/), in the + future hopefully we can add support for it in the form of an + independent makefile. + +- **Should I use this?** Maybe. This project was intended as a + proof-of-concept; it was motivated by my feeling that build tools in + the Scala world tend to be very complex machines, and that using + them to their full extent requires almost as much effort as learning + a new language. + + My suggestion is that you should give this Makefile a shot if + + 1. You are new to Scala but familiar with programming and the unix + environment, and want a quick but thorough overview of the steps + involved in going from a source file to an executable. + + 2. You are building an **application**. This makefile is too + bare-bones for libraries (for example it does not support + multiple scala version or uploading to Maven Central). + + 3. You agree with the following sentiment, as + [cbt](https://github.com/cvogt/cbt/)'s author, Chris Vogt, puts + it: + + > Do you own your Build Tool or does your Build Tool own you? diff --git a/scripts/combinejars b/scripts/combinejars new file mode 100755 index 0000000..338bd6d --- /dev/null +++ b/scripts/combinejars @@ -0,0 +1,26 @@ +#!/bin/bash +# Usage: combinejars +# +# Combines contents of jars into a single destination jar. The +# destination jar will have all it entries dates set to the unix +# epoch, hence making it reproducible for the same input. +# +# Contents at same paths are overwritten, the latter overwriting the earlier. + +destination="$(realpath "$1")" +shift +sources=("$@") + +workdir="$(mktemp -d)" + +cleanup() { + rm -rf "$workdir" +} +trap cleanup EXIT + +for s in "${sources[@]}"; do + absolute=$(realpath "$s") + (cd "$workdir" || exit 1; unzip -uo "$absolute" > /dev/null) +done +find "$workdir" -exec touch -t 197001010000 {} \; +jar -cMf "$destination" -C "$workdir" . diff --git a/src/main.scala b/src/main.scala new file mode 100644 index 0000000..0e28710 --- /dev/null +++ b/src/main.scala @@ -0,0 +1,6 @@ +object Main extends App { + val username = args.headOption.getOrElse("anonymous") + val svg = identicon.svg(username) + System.err.println(s"Identicon for user '$username'") + println(svg) +} -- cgit v1.2.3