#!/usr/bin/env bash function print_help { echo echo 'Usage: warp4j [options] ' echo echo 'Turn JAR (java archive) into self-contained executable' echo echo 'Options:' echo ' -j, --java-version ' echo ' override JDK/JRE version' echo ' examples: "11", "11.0", "11.0.2", "11.0.2+9"' echo ' (default: 11)' echo ' -o, --output ' echo ' override output directory;' echo ' this is relative to current PWD' echo ' (default: ./warped)' echo ' --list show available java releases;' echo ' takes into consideration other options:' echo ' "--java-version", "--no-optimize", "--jvm-impl";' echo ' the output may be used to specify concrete' echo ' "--java-version"' echo ' --no-optimize use JRE instead of optimized JDK;' echo ' by default jdeps and jlink are used to create' echo ' optimized JDK for the particular jar;' echo ' JRE is always used for java 8' echo ' --pull check if more recent JDK/JRE distro is available;' echo ' by default latest cached version that matches' echo ' "--java-version" is used' echo ' --linux create binary for Linux' echo ' --macos create binary for macOS' echo ' --windows create binary for Windows' echo ' if no targets are specified then binaries for' echo ' all targets are created' echo ' --jvm-impl jvm implementation: hotspot or openj9' echo ' (default: hotspot)' echo ' --jvm-options ' echo ' passed to java like this:' echo ' "java -jar ";' echo ' use quotes when passing multiple options' echo " example: '-Xms512m -Xmx1024m'" echo ' -h, --help show this message' exit } # exit top level program from subshell trap "exit 1" TERM export TOP_PID=$$ function fail() { kill -s TERM $TOP_PID } # platform IDs LIN=linux MAC=macos WIN=windows # returns this platform ID function get_this_platform() { local this_platform="$(uname -s)" case $this_platform in Linux*) echo $LIN ;; Darwin*) echo $MAC ;; *) echo "Error: Unsupported platform $this_platform" >&2 fail ;; esac } # show help if no arguments specified if [[ $# -eq 0 ]]; then print_help exit fi # parse arguments POSITIONAL=() while [[ $# -gt 0 ]]; do key="$1" case $key in -h|--help) print_help exit ;; -j|--java-version) JAVA_VERSION="$2" JAVA_VERSION_OVERRIDEN=true shift 2 ;; -o|--output) OUTPUT_DIR_PATH="$2" shift 2 ;; --list) LIST_RELEASES=true shift ;; --no-optimize) NO_OPTIMIZE=true shift ;; --pull) PULL=true shift ;; --linux) TARGETS+=($LIN) shift ;; --macos) TARGETS+=($MAC) shift ;; --windows) TARGETS+=($WIN) shift ;; --jvm-impl) JVM_IMPL="$2" shift 2 ;; --jvm-options) JVM_OPTIONS="$2" shift 2 ;; -*|--*) # unsupported options echo "Error: Unsupported option $1" >&2 exit 1 ;; *) POSITIONAL+=("$1") # store positional arguments shift ;; esac done set -- "${POSITIONAL[@]}" # restore positional arguments THIS_PLATFORM=$(get_this_platform) # checks if all dependencies are available function check_deps() { local deps=( "awk" \ "curl" \ "file" \ "grep" \ "sed" \ "sort" \ "tar" \ "unzip" \ "warp-packer" \ ) local missing=() for d in ${deps[@]}; do if ! command -v $d &> /dev/null ; then missing+=($d) fi done if [[ $missing ]]; then echo -n "Error: Missing dependencies: " >&2 for d in ${missing[@]}; do echo -n "$d " >&2 done echo >&2 fail fi } # actually check dependencies check_deps # apart from options only one argument is allowed if [[ $# -gt 1 ]]; then echo "Error: Too many arguments: $@, expecting only jar name" >&2 exit 1 else JAR=$1 fi # checks if java version specified correctly function java_version_is_correct() { local pattern="^[0-9]+(\.[0-9]+(\.[0-9]+(\+[0-9]+)?)?)?$" local version=$1 if [[ $version =~ $pattern ]]; then return 0 else return 1 fi } # validate java version if [[ $JAVA_VERSION ]] && ! java_version_is_correct $JAVA_VERSION ; then echo "Error: JDK version \"$JAVA_VERSION\" is not correct" >&2 exit 1 fi JVM_IMPL_HOTSPOT=hotspot JVM_IMPL_OPENJ9=openj9 # validate jvm implementation if [[ $JVM_IMPL ]] && [[ $JVM_IMPL != $JVM_IMPL_HOTSPOT ]] && [[ $JVM_IMPL != $JVM_IMPL_OPENJ9 ]]; then echo "Error: jvm implementation \"$JVM_IMPL\" is not correct" >&2 exit 1 fi LATEST_LTS=11 # latest LTS java branch # default options test -z $JAVA_VERSION && JAVA_VERSION=$LATEST_LTS test -z $TARGETS && TARGETS=($LIN $MAC $WIN) test -z $JVM_IMPL && JVM_IMPL=$JVM_IMPL_HOTSPOT # returns java branch version function get_base_version() { local version=$1 echo `echo $version | cut -d"." -f1` } # actually set java branch JAVA_VERSION_BASE=$(get_base_version $JAVA_VERSION) DISTRO_TYPE_JRE=jre DISTRO_TYPE_JDK=jdk # chooses what to use, JDK or JRE function choose_distro_type() { if [[ $JAVA_VERSION_BASE == 8 ]] || [[ $NO_OPTIMIZE ]]; then echo $DISTRO_TYPE_JRE else echo $DISTRO_TYPE_JDK fi } # actually choose distro type JAVA_DISTRO_TYPE=$(choose_distro_type) # generates adoptopenjdk api url function api_url() { local request=$1 # info/binary local platform=$2 # windows/linux/macos # adoptopenjdk uses "mac" instead of "macos" if [[ $platform == "macos" ]]; then platform="mac" fi echo -n "https://api.adoptopenjdk.net/v2/\ $request/releases/openjdk$JAVA_VERSION_BASE?\ openjdk_impl=$JVM_IMPL&\ os=$platform&\ arch=x64&\ type=$JAVA_DISTRO_TYPE" } # requests info about all releases for given platform and java branch function fetch_distro_info() { local platform=$1 # platform ID local branch=$2 # 8/9/10/11... echo "Fetching $JVM_IMPL-$JAVA_DISTRO_TYPE-$branch info..." >&2 curl -s $(api_url info $platform) if [[ $? != 0 ]]; then echo "Error: Failed to fetch java $branch info" >&2 fail fi } # extracts all concrete java versions that match the version specified by user # from provided distro info function find_matched_versions() { local info=$1 local user_version=$2 # turning something like "11.0.1+13" into regexp like "^11\.0\.1\+13" local pattern="^"$(echo $user_version \ | sed -e 's/\./\\\./g' -e 's/\+/\\\+/g') local versions=$(echo "$info" \ | grep '"semver"' \ | sort --reverse --version-sort \ | awk '{print $2}' \ | sed -e 's/"//g' -e 's/,//') for v in ${versions[@]}; do if [[ $v =~ $pattern ]]; then echo $v fi done } # prints all concrete java versions that match the version specified by user function list_releases() { local info local matched local printed local platform="linux" # just picked any info=$(fetch_distro_info $platform $JAVA_VERSION_BASE) matched=$(find_matched_versions "$info" $JAVA_VERSION) if [[ $matched ]]; then echo "Releases that match $JAVA_VERSION:" for m in ${matched[@]}; do if [[ ${printed[@]} != *"$m"* ]]; then echo $m printed+=($m) fi done else echo "No releases that match $JAVA_VERSION" fi } # actually show matched releases if [[ $LIST_RELEASES ]]; then list_releases exit fi JAR_FILE_BASE_NAME=$(basename -- "$JAR") # "my-app.jAr" JAR_EXTENSION="${JAR_FILE_BASE_NAME##*.}" # "jAr" JAR_EXTENSION_LOWERCASE=$(printf "%s" "$JAR_EXTENSION" | tr '[:upper:]' '[:lower:]') # "jar" JAR_NAME="${JAR_FILE_BASE_NAME%.*}" # "my-app" APP_NAME=$JAR_NAME # final binary name LAUNCHER_NAME=$JAR_NAME # launcher name inside bundle # checking jar file exists if [[ ! -e $JAR ]]; then echo "Error: File \"$JAR\" does not exist" >&2 exit 1 fi # checking file is actually java archive if ([[ $(file $JAR) != *"Java"* ]] && # it could be "Java archive data" or "Java Jar file data (zip)" [[ $(file $JAR) != *"Zip"* ]]) || # or "Zip archive data" [[ $JAR_EXTENSION_LOWERCASE != "jar" ]]; then echo "Error: File \"$JAR\" is not a java archive" >&2 exit 1 fi # even if this platform is not targeted, we still need # a JDK for this platform to optimize JDKs for other platforms TARGETS_TO_CACHE=${TARGETS[@]} if [[ $JAVA_DISTRO_TYPE == $DISTRO_TYPE_JDK ]] && # if usind JDK (not JRE) [[ ${TARGETS[@]} != *"$THIS_PLATFORM"* ]]; then # and this platform is not targeted TARGETS_TO_CACHE+=($THIS_PLATFORM) fi # choose cache path for this platform case $THIS_PLATFORM in $MAC) CACHE_PATH="$HOME/Library/Application Support/warp4j" ;; *) CACHE_PATH="$HOME/.local/share/warp4j" ;; esac # cache path can be overriden with environment variable if [[ $WARP4J_CACHE ]]; then CACHE_PATH=$WARP4J_CACHE fi # this is not full path, platform name and full version will be added JAVA_DOWNLOAD_PATH=$CACHE_PATH/$JAVA_DISTRO_TYPE/$JVM_IMPL BUNDLES_PATH=$CACHE_PATH/bundle # prepare bundles here DIR="$(pwd -P)" # execution directory path # final binaries created in WARPED_TEMP_PATH and then moved to WARPED_PATH WARPED_TEMP_PATH=$CACHE_PATH/out-temp if [[ -z $OUTPUT_DIR_PATH ]]; then WARPED_PATH=$DIR/warped else WARPED_PATH=$OUTPUT_DIR_PATH fi BUNDLED_DISTRO_SUBDIR="java" # runtime directory inside a bundle # prints a launcher for bash function print_launcher_bash() { printf "%s" \ '#!/usr/bin/env bash JAVA_DIST='"$BUNDLED_DISTRO_SUBDIR"' JAR='"$JAR_NAME"'.jar DIR="$(cd "$(dirname "$0")" ; pwd -P)" JAVA=$DIR/$JAVA_DIST/bin/java JAR_PATH=$DIR/$JAR exec "$JAVA" '"$JVM_OPTIONS"' -jar "$JAR_PATH" "$@" ' } # prints a launcher for windows cmd function print_launcher_cmd() { printf "%s" \ '@ECHO OFF SETLOCAL SET "JAVA_DIST='"$BUNDLED_DISTRO_SUBDIR"'" SET "JAR='"$JAR_NAME"'.jar" SET "JAVA=%~dp0\%JAVA_DIST%\bin\java.exe" SET "JAR_PATH=%~dp0\%JAR%" CALL %JAVA% '"$JVM_OPTIONS"' -jar %JAR_PATH% %* EXIT /B %ERRORLEVEL% ' } # these files are success markers MARKER_DOWNLOADED="downloaded" # after runtime download MARKER_UNPACKED="unpacked" # after runtime uncompress # returns latest cached version that matches version specified by user function find_latest_cached() { local platform=$1 local user_version=$2 local platform_dir=$JAVA_DOWNLOAD_PATH/$platform/ # turning something like "11.0.1+13" into regexp like "^11\.0\.1\+13" local pattern="^"$(echo $user_version \ | sed -e 's/\./\\\./g' -e 's/\+/\\\+/g') local versions=$(ls -1 "$platform_dir" 2> /dev/null \ | sort --reverse --version-sort) local version for v in ${versions[@]}; do if [[ -e $platform_dir/$v/$MARKER_DOWNLOADED ]] && [[ $v =~ $pattern ]]; then version=$v break fi done if [[ $version ]]; then echo $version else return 1 fi } # finds latest concrete distro version that matches version specified by user function find_latest_version() { local info=$1 # info fetched from AdoptOpenJDK local user_version=$2 # version supplied by user is a template local matched_version # latest version that matches the template local versions # all versions versions=$(echo "$info" \ | grep '"semver"' \ | sort --reverse --version-sort \ | awk '{print $2}' \ | sed -e 's/"//g' -e 's/,//') # turning something like "11.0.1+13" into regexp like "^11\.0\.1\+13" # remember that user may provide shorter version like "11", "11.0", "11.0.1" local pattern="^"$(echo $user_version | sed -e 's/\./\\\./g' -e 's/\+/\\\+/g') for v in ${versions[@]}; do if [[ $v =~ $pattern ]]; then matched_version=$v break fi done if [[ -z $matched_version ]]; then echo "Error: Can't find distro that matches $user_version" >&2 fail fi echo $matched_version } # finds direct link to download concrete runtime version function find_distro_link() { local info=$1 # info fetched from AdoptOpenJDK local version=$2 # concrete distro version like "11.0.2+9" local link=$(echo "$info" \ | grep -B11 "\"semver\": \"$version\"" \ | grep "binary_link" \ | awk '{print $2}' \ | sed -e 's/"//g' -e 's/,//') if [[ -z $link ]]; then echo "Error: Can't find download link for $version" >&2 fail fi echo "$link" } # downloads runtime distro function download_distro() { local platform=$1 local version=$2 local link=$3 local download_dir=$JAVA_DOWNLOAD_PATH/$platform/$version echo "Downloading $JVM_IMPL-$JAVA_DISTRO_TYPE-$version-$platform..." rm -rf "$download_dir" mkdir -p "$download_dir" (cd "$download_dir" curl --progress-bar --location --remote-name "$link" if [[ $? == 0 ]]; then touch $MARKER_DOWNLOADED else echo "Error: Failed to download $JVM_IMPL-$JAVA_DISTRO_TYPE-$version-$platform" >&2 fail fi ) } # ensures required distro is in cache function ensure_distro_cached() { local platform=$1 local distro_info local distro_link if [[ -z $PULL ]]; then if [[ -z $JAVA_VERSION_OVERRIDEN ]]; then if [[ ! $(find_latest_cached $platform $LATEST_LTS) ]]; then distro_info=$(fetch_distro_info $platform $LATEST_LTS) CONCRETE_JAVA_VERSION=$(find_latest_version "$distro_info" $LATEST_LTS) distro_link=$(find_distro_link "$distro_info" $CONCRETE_JAVA_VERSION) download_distro $platform $CONCRETE_JAVA_VERSION "$distro_link" else CONCRETE_JAVA_VERSION=$(find_latest_cached $platform $LATEST_LTS) fi else if [[ ! $(find_latest_cached $platform $JAVA_VERSION) ]]; then distro_info=$(fetch_distro_info $platform $JAVA_VERSION_BASE) CONCRETE_JAVA_VERSION=$(find_latest_version "$distro_info" $JAVA_VERSION) distro_link=$(find_distro_link "$distro_info" $CONCRETE_JAVA_VERSION) download_distro $platform $CONCRETE_JAVA_VERSION "$distro_link" else CONCRETE_JAVA_VERSION=$(find_latest_cached $platform $JAVA_VERSION) fi fi else if [[ -z $JAVA_VERSION ]]; then distro_info=$(fetch_distro_info $platform $LATEST_LTS) CONCRETE_JAVA_VERSION=$(find_latest_version "$distro_info" $LATEST_LTS) else distro_info=$(fetch_distro_info $platform $JAVA_VERSION_BASE) CONCRETE_JAVA_VERSION=$(find_latest_version "$distro_info" $JAVA_VERSION) fi if [[ ! $(find_latest_cached $platform $CONCRETE_JAVA_VERSION) ]]; then distro_link=$(find_distro_link "$distro_info" $CONCRETE_JAVA_VERSION) download_distro $platform $CONCRETE_JAVA_VERSION "$distro_link" fi fi } # actually ensure required distro is in cache for target in ${TARGETS_TO_CACHE[@]}; do ensure_distro_cached $target done UNPACKED_SUBDIR="distro" # ensures required distro uncompressed function ensure_distro_unpacked() { local platform=$1 local version=$2 local download_dir=$JAVA_DOWNLOAD_PATH/$platform/$version local unpacked_dir=$download_dir/$UNPACKED_SUBDIR if [[ ! -e $download_dir/$MARKER_UNPACKED ]]; then echo "Uncompressing $JVM_IMPL-$JAVA_DISTRO_TYPE-$version-$platform" # removing all leftover directories for d in "$download_dir"/* ; do if [[ -d $d ]]; then rm -rf "$d" fi done case $platform in $LIN) mkdir -p "$unpacked_dir" tar --strip-components=1 -C "$unpacked_dir" -xzf "$download_dir"/*.tar.gz ;; $MAC) # to uncompess distro for macOS we need to use wildcards # to use wildcards with GNU tar (on Linux) '--wildcard' option is required # to use wildcards with BSD tar (on macOS) no options required # if invoked with '--wildcards' BSD tar with exit with message: # "Option --wildcards is not supported" local tar_cmd if tar --wildcards 2>&1 | grep "not supported" > /dev/null ; then tar_cmd="tar" else tar_cmd="tar --wildcards" fi mkdir -p "$unpacked_dir" $tar_cmd --strip-components=3 -C "$unpacked_dir" -xzf "$download_dir"/*.tar.gz \ "jdk*/Contents/Home" ;; $WIN) (cd "$download_dir" unzip -oq *.zip && mv jdk* $UNPACKED_SUBDIR ) ;; esac if [[ $? == 0 ]]; then touch "$download_dir/$MARKER_UNPACKED" else echo "Error: Failed to unpack $JVM_IMPL-$JAVA_DISTRO_TYPE-$version-$platform" >&2 fail fi fi } # actually ensure required distro uncompressed for target in ${TARGETS[@]}; do ensure_distro_unpacked $target $CONCRETE_JAVA_VERSION done JDK_PATH=$JAVA_DOWNLOAD_PATH/$THIS_PLATFORM/$CONCRETE_JAVA_VERSION/$UNPACKED_SUBDIR JLINK=$JDK_PATH/bin/jlink JDEPS=$JDK_PATH/bin/jdeps # modules are only needed if JDK optimisation is performed if [[ $JAVA_DISTRO_TYPE == $DISTRO_TYPE_JDK ]]; then echo "Analyzing dependencies..." # TODO check for errors MODULES=$("$JDEPS" --print-module-deps "$JAR" | grep -v Warning) fi # creates minimized runtime for the platform function create_optimized_runtime() { local platform=$1 local jmods=$JAVA_DOWNLOAD_PATH/$platform/$CONCRETE_JAVA_VERSION/$UNPACKED_SUBDIR/jmods echo "Creating minimal runtime for $platform..." "$JLINK" \ --no-header-files \ --no-man-pages \ --strip-debug \ --module-path "$jmods" \ --add-modules $MODULES \ --output "$BUNDLES_PATH/$platform/$BUNDLED_DISTRO_SUBDIR" if [[ $? != 0 ]]; then echo "Error: Failed to optimize runtime" >&2 fail fi } # creates warp bundle for the platform function create_bundle() { local platform=$1 case $JAVA_DISTRO_TYPE in $DISTRO_TYPE_JDK) create_optimized_runtime $platform ;; $DISTRO_TYPE_JRE) mkdir -p "$BUNDLES_PATH/$platform/$BUNDLED_DISTRO_SUBDIR" cp -r "$JAVA_DOWNLOAD_PATH/$platform/$CONCRETE_JAVA_VERSION/$UNPACKED_SUBDIR"/* \ "$BUNDLES_PATH/$platform/$BUNDLED_DISTRO_SUBDIR" ;; esac case $platform in $WIN) print_launcher_cmd > "$BUNDLES_PATH/$platform/$LAUNCHER_NAME.cmd" ;; *) print_launcher_bash > "$BUNDLES_PATH/$platform/$LAUNCHER_NAME.sh" chmod +x "$BUNDLES_PATH/$platform/$LAUNCHER_NAME.sh" ;; esac cp "$JAR" "$BUNDLES_PATH/$platform/" } # remove old bundles rm -rf "$BUNDLES_PATH" # actually create bundles for all targets for target in ${TARGETS[@]}; do create_bundle $target done # creates binaries and archives for all targets function warp_targets() { mkdir -p "$WARPED_PATH" if [[ ${TARGETS[*]} == *"$LIN"* ]]; then echo "Warping for $LIN..." mkdir -p "$WARPED_TEMP_PATH/$LIN" warp-packer \ --arch linux-x64 \ --input_dir "$BUNDLES_PATH/$LIN" \ --exec "$LAUNCHER_NAME.sh" \ --output "$WARPED_TEMP_PATH/$LIN/$APP_NAME" \ &> /dev/null if [[ $? != 0 ]]; then echo "Error: Failed to warp for $LIN" >&2 fail fi echo "Archiving for $LIN..." tar -C "$WARPED_TEMP_PATH/$LIN" -czf "$WARPED_TEMP_PATH/$APP_NAME-$LIN-x64.tar.gz" "$APP_NAME" if [[ $? != 0 ]]; then echo "Error: Failed to make archive for $LIN" >&2 fail fi mv "$WARPED_TEMP_PATH/$LIN/$APP_NAME" "$WARPED_PATH/$APP_NAME-$LIN-x64" mv "$WARPED_TEMP_PATH/$APP_NAME-$LIN-x64.tar.gz" "$WARPED_PATH" rmdir "$WARPED_TEMP_PATH/$LIN" fi if [[ ${TARGETS[*]} == *"$MAC"* ]]; then echo "Warping for $MAC..." mkdir -p "$WARPED_TEMP_PATH/$MAC" warp-packer \ --arch macos-x64 \ --input_dir "$BUNDLES_PATH/$MAC" \ --exec "$LAUNCHER_NAME.sh" \ --output "$WARPED_TEMP_PATH/$MAC/$APP_NAME" \ &> /dev/null if [[ $? != 0 ]]; then echo "Error: Failed to warp for $MAC" >&2 fail fi echo "Archiving for $MAC..." tar -C "$WARPED_TEMP_PATH/$MAC" -czf "$WARPED_TEMP_PATH/$APP_NAME-$MAC-x64.tar.gz" "$APP_NAME" if [[ $? != 0 ]]; then echo "Error: Failed to make archive for $MAC" >&2 fail fi mv "$WARPED_TEMP_PATH/$MAC/$APP_NAME" "$WARPED_PATH/$APP_NAME-$MAC-x64" mv "$WARPED_TEMP_PATH/$APP_NAME-$MAC-x64.tar.gz" "$WARPED_PATH" rmdir "$WARPED_TEMP_PATH/$MAC" fi if [[ ${TARGETS[*]} == *"$WIN"* ]]; then echo "Warping for $WIN..." mkdir -p "$WARPED_TEMP_PATH/$WIN" warp-packer \ --arch windows-x64 \ --input_dir "$BUNDLES_PATH/$WIN" \ --exec "$LAUNCHER_NAME.cmd" \ --output "$WARPED_TEMP_PATH/$WIN/$APP_NAME.exe" \ &> /dev/null if [[ $? != 0 ]]; then echo "Error: Failed to warp for $WIN" >&2 fail fi if command -v zip &> /dev/null ; then ( echo "Archiving for $WIN..." cd "$WARPED_TEMP_PATH/$WIN" zip -r "$WARPED_TEMP_PATH/$APP_NAME-$WIN-x64.zip" "$APP_NAME.exe" &> /dev/null if [[ $? != 0 ]]; then echo "Error: Failed to make archive for $WIN" >&2 fail fi mv "$WARPED_TEMP_PATH/$APP_NAME-$WIN-x64.zip" "$WARPED_PATH" ) else echo "Warning: 'zip' not found, will skip creation of archive for windows" >&2 fi mv "$WARPED_TEMP_PATH/$WIN/$APP_NAME.exe" "$WARPED_PATH/$APP_NAME-windows-x64.exe" rmdir "$WARPED_TEMP_PATH/$WIN" fi rmdir "$WARPED_TEMP_PATH" } # actually create binaries and archives for all targets warp_targets