Skip to content

Instantly share code, notes, and snippets.

@mcgivrer
Last active February 14, 2026 23:25
Show Gist options
  • Select an option

  • Save mcgivrer/66e46be75ef7a42ba2a15fd357c80d9e to your computer and use it in GitHub Desktop.

Select an option

Save mcgivrer/66e46be75ef7a42ba2a15fd357c80d9e to your computer and use it in GitHub Desktop.
Java+Build+Package+JRE to build and package a Java project with Jinput and LWJGL

Build and Package Script

Standalone build script to compile, package and distribute a Java project with natives dependencies like LWJGL and JInput.

Prerequisites

  • Java JDK 25 (or version specified in SOURCE_VERSION)
  • SDKMAN (optional, for automatic Java installation)
  • curl or wget (to download dependencies)
  • zip (to create the distribution archive)

Usage

# Standard compilation
./build

# Compilation + run tests
./build test

# Compilation + run application
./build run

# Compilation + create minimal JRE + distribution archive
./build jre

Options

Option Description
(none) Compiles the project and creates the JAR
test Compiles and runs JUnit tests
run Compiles and launches the application
jre Creates an embedded minimal JRE + distributable ZIP archive

Required Project Structure

project/
├── build                    # Build script (this file)
├── src/
│   ├── main/
│   │   ├── java/           # Java sources
│   │   └── resources/      # Resources (images, shaders, config...)
│   └── test/
│       ├── java/           # Test sources
│       └── resources/      # Test resources
├── libs/                    # Dependencies (created automatically)
│   ├── lwjgl/              # LWJGL JARs (downloaded)
│   ├── jinput/             # JInput JARs (downloaded)
│   └── junit-platform-console-standalone-6.0.0.jar  # For tests
└── target/                  # Build output (generated)
    ├── classes/            # Compiled classes
    ├── test-classes/       # Compiled test classes
    └── build/              # Distribution package
        ├── libs/           # Runtime dependencies
        ├── natives/        # Native libraries (.so, .dll, .dylib)
        ├── jre/            # Minimal JRE (with jre option)
        ├── GameFront-x.x.x.jar
        └── GameFront.sh    # Launcher script

Configuration

Project parameters are defined at the top of the script:

project_name=GameFront           # Project name
project_version=0.0.5            # Version
main_class=core.App              # Main class
vendor_name="SnapGames"          # Vendor
author_name="..."                # Author
SOURCE_VERSION="25"              # Required Java version
LWJGL_VERSION="3.3.3"            # LWJGL version
JINPUT_VERSION="2.0.10"          # JInput version

Output with jre Option

The jre option creates a self-contained distribution package:

target/
├── build/
│   ├── jre/                # Minimal JRE (~40-50 MB)
│   ├── libs/               # Dependencies
│   ├── natives/            # Native libraries
│   ├── GameFront-0.0.5.jar
│   └── GameFront.sh        # Self-executable launcher
└── GameFront-0.0.5-linux.zip   # Distributable archive

Included JRE Modules

The minimal JRE contains only the required modules:

  • java.base - Core Java
  • java.desktop - AWT/Swing (required by LWJGL)
  • java.datatransfer - Clipboard
  • java.xml - XML parsing
  • java.prefs - Preferences API
  • java.logging - Logging
  • jdk.unsupported - sun.misc.Unsafe (required by LWJGL)

Distribution

The generated ZIP archive is self-contained and can be distributed directly. The end user does not need to install Java.

To run on the target machine:

# Extract the archive
unzip GameFront-0.0.5-linux.zip -d GameFront

# Launch the game
cd GameFront
./GameFront.sh

Supported Platforms

The script automatically detects the platform and downloads the appropriate natives:

Platform Natives suffix
Linux natives-linux
macOS Intel natives-macos
macOS ARM natives-macos-arm64
Windows natives-windows

Automatically Downloaded Dependencies

LWJGL 3.3.3

  • lwjgl - Core
  • lwjgl-glfw - Windowing
  • lwjgl-opengl - OpenGL bindings
  • lwjgl-stb - Image loading

JInput 2.0.10

  • jinput - Input handling (joysticks, gamepads)
  • jinput-natives-all - Natives for all platforms

Troubleshooting

Java not detected

The script attempts to use SDKMAN if available. Otherwise, install Java 25 manually.

Download error

Check your internet connection and that curl or wget is installed.

jlink error

Make sure you are using a JDK (not a JRE) to create the minimal JRE.

Missing archive

Install zip: sudo apt install zip (Debian/Ubuntu) or brew install zip (macOS).

#!/bin/bash
#---- project parameters
project_name=[ProjectName]
project_version=[ProjectVersion]
main_class=[ProjectMainClass]
vendor_name="[PorojectVendorName]"
author_name="[AuthorName]<[AuthorEmail]>"
SOURCE_VERSION="25"
JAR_OPTS="-Xmx512m --enable-native-access=ALL-UNNAMED"
#--- external dependencies version
LWJGL_VERSION="3.3.3"
JINPUT_VERSION="2.0.10"
# Detect OS and architecture
OS_NAME=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m | tr '[:upper:]' '[:lower:]')
case "$OS_NAME" in
linux*)
OS="linux" ;;
darwin*)
if [[ "$ARCH" == "arm64" ]]; then
OS="macos-arm64"
else
OS="macos"
fi
;;
msys*|mingw*|cygwin*)
OS="windows" ;;
*)
OS="linux" ;; # default to linux
esac
# Define natives based on OS
NATIVES_SUFFIX="natives-${OS}"
# JInput natives suffix mapping
case "$OS" in
linux)
JINPUT_NATIVES="natives-linux" ;;
macos|macos-arm64)
JINPUT_NATIVES="natives-osx" ;;
windows)
JINPUT_NATIVES="natives-windows" ;;
*)
JINPUT_NATIVES="natives-linux" ;;
esac
# detect OS type to set the classpath separator
if [[ "$OSTYPE" == "linux"* ]] || [[ "$OSTYPE" == "darwin"* ]]; then
FS=":"
else
FS=";"
fi
# LWJGL Jars
LWJGL_MODULES=("lwjgl" "lwjgl-glfw" "lwjgl-opengl" "lwjgl-stb")
LWJGL_JARS=""
JARS=""
# Function to download JAR from Maven Central
download_jar() {
local group=$1
local artifact=$2
local version=$3
local classifier=$4
local dest_dir=$5
local url="https://repo1.maven.org/maven2/${group//.//}/${artifact}/${version}/${artifact}-${version}${classifier:+-$classifier}.jar"
local filename="${artifact}-${version}${classifier:+-$classifier}.jar"
local dest_path="${dest_dir}/${filename}"
if [ ! -f "$dest_path" ]; then
echo "Downloading ${filename}..."
mkdir -p "$dest_dir"
if command -v curl >/dev/null 2>&1; then
curl -L -o "$dest_path" "$url"
elif command -v wget >/dev/null 2>&1; then
wget -O "$dest_path" "$url"
else
echo "Error: Neither curl nor wget is available for downloading."
exit 1
fi
else
echo "${filename} already exists."
fi
}
# Download LWJGL jars if missing
LWJGL_DIR="libs/lwjgl"
mkdir -p "$LWJGL_DIR"
for module in "${LWJGL_MODULES[@]}"; do
download_jar "org.lwjgl" "$module" "$LWJGL_VERSION" "" "$LWJGL_DIR"
download_jar "org.lwjgl" "$module" "$LWJGL_VERSION" "$NATIVES_SUFFIX" "$LWJGL_DIR"
jar_path="${LWJGL_DIR}/${module}-${LWJGL_VERSION}.jar"
natives_path="${LWJGL_DIR}/${module}-${LWJGL_VERSION}-${NATIVES_SUFFIX}.jar"
LWJGL_JARS="${LWJGL_JARS}${LWJGL_JARS:+${FS}}${jar_path}${FS}${natives_path}"
JARS="${JARS}${JARS:+ }${jar_path} ${natives_path}"
done
# Download JInput jars if missing
JINPUT_DIR="libs/jinput"
mkdir -p "$JINPUT_DIR"
# JInput core JAR
download_jar "net.java.jinput" "jinput" "$JINPUT_VERSION" "" "$JINPUT_DIR"
JINPUT_JAR="${JINPUT_DIR}/jinput-${JINPUT_VERSION}.jar"
JARS="${JARS}${JARS:+ }${JINPUT_JAR}"
# JInput natives (all platforms in one JAR)
download_jar "net.java.jinput" "jinput" "$JINPUT_VERSION" "natives-all" "$JINPUT_DIR"
JINPUT_NATIVES_JAR="${JINPUT_DIR}/jinput-${JINPUT_VERSION}-natives-all.jar"
JARS="${JARS}${JARS:+ }${JINPUT_NATIVES_JAR}"
# Add JInput to external jars
EXTERNAL_JARS="${LWJGL_JARS}${FS}${JINPUT_JAR}${FS}${JINPUT_NATIVES_JAR}"
#
#--- DO NOT CHANGE THE FOLLOWING LINES ---
#
main_class_array=($main_class)
num_main_classes=${#main_class_array[@]}
SRC=./src
LIBS=./libs
TARGET=./target
BUILD=${TARGET}/build
CLASSES=${TARGET}/classes
RESOURCES=${SRC}/main/resources
SOURCE_ENCODING="UTF-8"
JARS=libs/dependencies/
COMPILATION_OPTS="-Xlint:unchecked -Xlint:deprecation -parameters"
# add your test execution commands here
TEST_CLASSES=${TARGET}/test-classes
TEST_RESOURCES=${SRC}/test/resources
LIB_TEST=$LIBS/junit-platform-console-standalone-6.0.0.jar
# define colors
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
#---- process buid
GIT_COMMIT_ID=$(git rev-parse HEAD)
JAVA_BUILD=$(java --version | head -1 | cut -f2 -d' ')
#---- check SDKMAN
REQUIRED_JAVA_VERSION=$SOURCE_VERSION
JAVA_BIN=$(command -v java 2>/dev/null)
JAVA_VERSION_DETECTED=$($JAVA_BIN -version 2>&1 | awk -F '[\"_]' '/version/ {print $2}' | cut -d'.' -f1)
if [ -z "$JAVA_BIN" ] || [ "$JAVA_VERSION_DETECTED" != "$REQUIRED_JAVA_VERSION" ]; then
echo -e "${RED}Java n'est pas installé. Tentative d'installation de Java $REQUIRED_JAVA_VERSION avec SDKMAN.${NC}"
# Ensure the `sdk` command is available by sourcing SDKMAN init if installed
if [ -z "$(command -v sdk 2>/dev/null)" ] && [ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]; then
# shellcheck source=/dev/null
source "$HOME/.sdkman/bin/sdkman-init.sh"
if [ -f .sdkmanrc ]; then
echo "Install SDKMAN environment"
sdk env install
sdk env use
echo "done."
fi
fi
else
echo -e "${GREEN}Java détecté: version $JAVA_VERSION_DETECTED${NC}"
fi
# Prepare Build
rm -vrf target/
find $SRC/main/java $RESOURCES -name "*.java"
mkdir -vp ${TARGET}/{classes,build/libs}
# Copy runtime dependencies
echo "copy runtime dependencies to ${TARGET}/build/libs"
cp ${LWJGL_DIR}/*.jar ${TARGET}/build/libs/
cp ${JINPUT_DIR}/*.jar ${TARGET}/build/libs/
# Extract JInput native libraries
NATIVES_DIR="${TARGET}/build/natives"
mkdir -p "${NATIVES_DIR}"
echo "Extracting JInput native libraries to ${NATIVES_DIR}..."
unzip -o -j "${JINPUT_DIR}/jinput-${JINPUT_VERSION}-natives-all.jar" "*.so" "*.dll" "*.dylib" "*.jnilib" -d "${NATIVES_DIR}" 2>/dev/null || true
# Compile sources
javac ${COMPILATION_OPTS} -cp "${TARGET}/build/libs/*" $(find $SRC/main/java $RESOURCES -name "*.java") -d ${CLASSES}
# create MANIFEST file
cp -vr $RESOURCES/* $CLASSES
echo "build jar..."
for app in "${main_class_array[@]}"; do
if [ $num_main_classes -eq 1 ]; then
jar_name="${project_name}-${project_version}.jar"
else
jar_name="${project_name}-$app-${project_version}.jar"
fi
mkdir -p ${TARGET}/META-INF
echo ">> for ${project_name}.$app..."
echo """
Manifest-Version: ${project_name}
Main-Class: ${app}
Class-Path: ${JARS}
Created-By: ${JAVA_BUILD}
Implementation-Title: ${project_name}
Implementation-Version: ${project_version}-build_${GIT_COMMIT_ID:0:12}
Implementation-Vendor: ${vendor_name}
Implementation-Author: ${author_name}
""" >>${TARGET}/META-INF/MANIFEST.MF
jar cvfe ${TARGET}/build/${jar_name} $app -C ${CLASSES} .
# create run script
if [[ "$OS" == "windows" ]]; then
script_name="${project_name}.bat"
echo "create run script ${script_name} ..."
rm -f ${script_name}
echo """@echo off
java -Djava.library.path=\"${TARGET}/build/natives\" -cp \"${TARGET}/build/libs/*${FS}${TARGET}/build/${jar_name}\" core.App %*
""" >${script_name}
else
script_name="${project_name}.sh"
echo "create run script ${script_name} ..."
rm -f ${script_name}
echo """#!/bin/bash
java -Djava.library.path=\"${TARGET}/build/natives\" -cp \"${TARGET}/build/libs/*:${TARGET}/build/${jar_name}\" core.App \$@
""" >${script_name}
chmod +x ${script_name}
echo "done."
fi
echo "done."
done
if ([ "$1" == "jre" ]); then
echo "Creating minimal JRE..."
JRE_DIR="${TARGET}/build/jre"
JRE_MODULES="java.base,java.datatransfer,java.xml,java.prefs,java.desktop,java.logging,jdk.unsupported"
# Remove existing JRE if present
rm -rf "${JRE_DIR}"
# Create minimal JRE with jlink
echo "Running jlink to create minimal JRE with modules: ${JRE_MODULES}"
jlink --no-header-files \
--no-man-pages \
--compress=zip-6 \
--strip-debug \
--add-modules "${JRE_MODULES}" \
--output "${JRE_DIR}"
if [ $? -eq 0 ]; then
echo -e "${GREEN}Minimal JRE created successfully in ${JRE_DIR}${NC}"
echo "JRE size: $(du -sh ${JRE_DIR} | cut -f1)"
# Create self-contained launcher script in target/build
jar_name="${project_name}-${project_version}.jar"
if [[ "$OS" == "windows" ]]; then
launcher_name="${TARGET}/build/${project_name}.bat"
echo "Creating launcher script ${launcher_name} ..."
cat > "${launcher_name}" << 'EOFBAT'
@echo off
setlocal
set SCRIPT_DIR=%~dp0
"%SCRIPT_DIR%jre\bin\java" -Djava.library.path="%SCRIPT_DIR%natives" -cp "%SCRIPT_DIR%libs\*;%SCRIPT_DIR%JARNAME" core.App %*
endlocal
EOFBAT
sed -i "s/JARNAME/${jar_name}/g" "${launcher_name}"
else
launcher_name="${TARGET}/build/${project_name}.sh"
echo "Creating launcher script ${launcher_name} ..."
cat > "${launcher_name}" << 'EOFSH'
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"${SCRIPT_DIR}/jre/bin/java" -Djava.library.path="${SCRIPT_DIR}/natives" -cp "${SCRIPT_DIR}/libs/*:${SCRIPT_DIR}/JARNAME" core.App "$@"
EOFSH
sed -i "s/JARNAME/${jar_name}/g" "${launcher_name}"
chmod +x "${launcher_name}"
fi
echo -e "${GREEN}Self-contained launcher created: ${launcher_name}${NC}"
echo ""
echo -e "${GREEN}Distribution package ready in ${TARGET}/build/${NC}"
echo "Contents:"
echo " - jre/ : Minimal Java Runtime"
echo " - libs/ : Dependencies"
echo " - natives/ : Native libraries"
echo " - ${jar_name} : Application JAR"
echo " - ${project_name}.sh : Launcher script"
echo ""
echo "Total size: $(du -sh ${TARGET}/build | cut -f1)"
# Create distribution zip archive
echo ""
echo "Creating distribution archive..."
DIST_NAME="${project_name}-${project_version}-${OS}"
DIST_ZIP="${TARGET}/${DIST_NAME}.zip"
rm -f "${DIST_ZIP}"
# Create zip from target/build directory
pushd "${TARGET}/build" > /dev/null
zip -r "../${DIST_NAME}.zip" . -x "*.zip"
popd > /dev/null
if [ -f "${DIST_ZIP}" ]; then
echo -e "${GREEN}Distribution archive created: ${DIST_ZIP}${NC}"
echo "Archive size: $(du -sh ${DIST_ZIP} | cut -f1)"
else
echo -e "${RED}Failed to create distribution archive${NC}"
fi
else
echo -e "${RED}Failed to create minimal JRE${NC}"
exit 1
fi
echo "done."
fi
if ([ "$1" == "test" ]); then
echo "Run tests..."
echo -e "|_ ${BLUE}6. Execute tests${NC}..."
echo "> from : ${SRC}/test"
echo "> to : ${TARGET}/test-classes"
mkdir -p ${TARGET}/test-classes
echo "copy test resources"
cp -r ./$RESOURCES/* $TEST_CLASSES
cp -r ./$TEST_RESOURCES/* $TEST_CLASSES
echo "compile test classes"
#list test sources
find ${SRC}/main -name '*.java' >${TARGET}/sources.lst
find ${SRC}/test -name '*.java' >${TARGET}/test-sources.lst
javac -source $SOURCE_VERSION -encoding $SOURCE_ENCODING $COMPILATION_OPTS -cp ".${FS}$LIB_TEST${FS}${EXTERNAL_JARS}" -d $TEST_CLASSES @${TARGET}/sources.lst @${TARGET}/test-sources.lst
echo "execute tests through JUnit"
java $JAR_OPTS -jar $LIB_TEST execute -cp "${EXTERNAL_JARS}${FS}${CLASSES}${FS}${TEST_CLASSES}${FS}." --scan-class-path
echo -e " |_ ${GREEN}done$NC"
echo "- execute tests through JUnit ${SRC}/test." >>${TARGET}/build.log
fi
if ([ "$1" == "run" ]); then
echo "Run the generated JAR(s)..."
for app in "${main_class_array[@]}"; do
if [ $num_main_classes -eq 1 ]; then
jar_name="${project_name}-${project_version}.jar"
else
jar_name="${project_name}-$app-${project_version}.jar"
fi
echo ">> run JAR ${jar_name} ..."
shift 1
java $JAR_OPTS -Djava.library.path="${TARGET}/build/natives" -cp "${TARGET}/build/libs/*${FS}${TARGET}/build/${jar_name}" core.App $@
echo "done."
done
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment