com.badlogicgames.packr.PackrReduce Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of packr-all Show documentation
Show all versions of packr-all Show documentation
A single executable jar for use from the command line. Packages your JAR, assets and a JVM for distribution on Windows, Linux and macOS, adding a native executable file to make it appear like a native app.
/*
* Copyright 2020 See AUTHORS file
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.badlogicgames.packr;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.compressors.CompressorException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import static com.badlogicgames.packr.ArchiveUtils.ArchiveType.ZIP;
/**
* Functions to reduce package size for both classpath JARs, and the bundled JRE.
*/
class PackrReduce {
/**
* Tries to shrink the size of the JRE by deleting unused files and possibly removing items from the included jars of the JRE.
*
* @param output the directory to save the minimized JRE into
* @param config the options for minimizing the JRE
*
* @throws IOException if an IO error occurs
* @throws ArchiveException if an archive error occurs
* @throws CompressorException if a compression error occurs
*/
static void minimizeJre(File output, PackrConfig config) throws IOException, CompressorException, ArchiveException {
if (config.minimizeJre == null) {
return;
}
System.out.println("Minimizing JRE ...");
JsonObject minimizeJson = readMinimizeProfile(config);
if (minimizeJson != null) {
if (config.verbose) {
System.out.println(" # Removing files and directories in profile '" + config.minimizeJre + "' ...");
}
JsonArray reduceArray = minimizeJson.get("reduce").asArray();
for (JsonValue reduce : reduceArray) {
String path = reduce.asObject().get("archive").asString();
File file = new File(output, path);
if (!file.exists()) {
if (config.verbose) {
System.out.println(" # No file or directory '" + file.getPath() + "' found, skipping");
}
continue;
}
boolean needsUnpack = !file.isDirectory();
File fileNoExt = needsUnpack ? new File(output, path.contains(".") ? path.substring(0, path.lastIndexOf('.')) : path) : file;
if (needsUnpack) {
if (config.verbose) {
System.out.println(" # Unpacking '" + file.getPath() + "' ...");
}
ArchiveUtils.extractArchive(file.toPath(), fileNoExt.toPath());
}
JsonArray removeArray = reduce.asObject().get("paths").asArray();
for (JsonValue remove : removeArray) {
File removeFile = new File(fileNoExt, remove.asString());
if (removeFile.exists()) {
if (removeFile.isDirectory()) {
PackrFileUtils.deleteDirectory(removeFile);
} else {
Files.deleteIfExists(removeFile.toPath());
}
} else {
if (config.verbose) {
System.out.println(" # No file or directory '" + removeFile.getPath() + "' found");
}
}
}
if (needsUnpack) {
if (config.verbose) {
System.out.println(" # Repacking '" + file.getPath() + "' ...");
}
createZipFileFromDirectory(config, file, fileNoExt);
}
}
JsonArray removeArray = minimizeJson.get("remove").asArray();
for (JsonValue remove : removeArray) {
String platform = remove.asObject().get("platform").asString();
if (!matchPlatformString(platform, config)) {
continue;
}
JsonArray removeFilesArray = remove.asObject().get("paths").asArray();
for (JsonValue removeFile : removeFilesArray) {
removeFileWildcard(output, removeFile.asString(), config);
}
}
}
}
/**
* Creates a new zip file {@code zipFileOutput} from the directory {@code directoryToZipAndThenDelete}. After the Zip file is successfully created, {@code
* directoryToZipAndThenDelete} is deleted.
*
* @param config configuration information
* @param zipFileOutput the Zip file to create
* @param directoryToZipAndThenDelete the contents to put into the created Zip file
*
* @throws IOException if an IO error occurs
* @throws ArchiveException if an archive error occurs
*/
private static void createZipFileFromDirectory(PackrConfig config, File zipFileOutput, File directoryToZipAndThenDelete)
throws IOException, ArchiveException {
long beforeLen = zipFileOutput.length();
Files.deleteIfExists(zipFileOutput.toPath());
ArchiveUtils.createArchive(ZIP, directoryToZipAndThenDelete.toPath(), zipFileOutput.toPath());
PackrFileUtils.deleteDirectory(directoryToZipAndThenDelete);
long afterLen = zipFileOutput.length();
if (config.verbose) {
System.out.println(" # " + beforeLen / 1024 + " kb -> " + afterLen / 1024 + " kb");
}
}
/**
* Checks {@code platform} matches what is specified in the {@code config}.
*
* @param platform the platform to check against {@code config}
* @param config check if the platform is in this config
*
* @return true if {@code platform} is the same as specified in {@code config}
*/
private static boolean matchPlatformString(String platform, PackrConfig config) {
return "*".equals(platform) || config.platform.desc.contains(platform);
}
/**
* Deletes the path named {@code removeFileWildcard} int directory {@code output}, or if {@code removeFileWildcard} contains a *, it looks for paths in
* {@code output} that start and end with those parts of {@code removeWildcard}.
*
* @param output the directory to look for {@code removeFileWildcard} named paths in and delete them
* @param removeFileWildcard either the exact name of a sub-path in {@code output} to delete, or paths that match the parts of a single wildcard
* pattern
* @param config the packr config
*
* @throws IOException if an IO error occurs
*/
private static void removeFileWildcard(File output, String removeFileWildcard, PackrConfig config) throws IOException {
if (removeFileWildcard.contains("*")) {
String removePath = removeFileWildcard.substring(0, removeFileWildcard.indexOf('*') - 1);
String removeSuffix = removeFileWildcard.substring(removeFileWildcard.indexOf('*') + 1);
File[] files = new File(output, removePath).listFiles();
if (files != null) {
for (File file : files) {
if (removeSuffix.isEmpty() || file.getName().endsWith(removeSuffix)) {
removeFile(file, config);
}
}
} else {
if (config.verbose) {
System.out.println(" # No matching files found in '" + removeFileWildcard + "'");
}
}
} else {
removeFile(new File(output, removeFileWildcard), config);
}
}
/**
* Deletes the file or directory {@code file}.
*
* @param file the file or directory to delete
* @param config packr configuration
*
* @throws IOException if an IO error occurs
*/
private static void removeFile(File file, PackrConfig config) throws IOException {
if (!file.exists()) {
if (config.verbose) {
System.out.println(" # No file or directory '" + file.getPath() + "' found");
}
return;
}
if (config.verbose) {
System.out.println(" # Removing '" + file.getPath() + "'");
}
if (file.isDirectory()) {
PackrFileUtils.deleteDirectory(file);
} else {
Files.deleteIfExists(file.toPath());
}
}
/**
* Loads the minimize configuration from {@link PackrConfig#minimizeJre} in {@code config}.
*
* @param config the config to find the minimize configuration for and load
*
* @return the minimize config in JSON format
*
* @throws IOException if an IO error occurs
*/
private static JsonObject readMinimizeProfile(PackrConfig config) throws IOException {
JsonObject json = null;
if (new File(config.minimizeJre).exists()) {
json = JsonObject.readFrom(new String(Files.readAllBytes(Paths.get(config.minimizeJre)), StandardCharsets.UTF_8));
} else {
InputStream in = Packr.class.getResourceAsStream("/minimize/" + config.minimizeJre);
if (in != null) {
json = JsonObject.readFrom(new InputStreamReader(in));
}
}
if (json == null && config.verbose) {
System.out.println(" # No minimize profile '" + config.minimizeJre + "' found");
}
return json;
}
/**
* Remove any dynamic libraries that don't match {@link PackrConfig#platform}.
*
* @param output the output configuration
* @param config the packr configuration
* @param removePlatformLibsFileFilter addition files to remove if they match
*
* @throws IOException if an IO error occurs
* @throws ArchiveException if an archive error occurs
* @throws CompressorException if a compression error occurs
*/
static void removePlatformLibs(PackrOutput output, PackrConfig config, Predicate removePlatformLibsFileFilter)
throws IOException, CompressorException, ArchiveException {
if (config.removePlatformLibs == null || config.removePlatformLibs.isEmpty()) {
return;
}
boolean extractLibs = config.platformLibsOutDir != null;
File libsOutputDir = null;
if (extractLibs) {
libsOutputDir = new File(output.executableFolder, config.platformLibsOutDir.getPath());
Files.createDirectories(libsOutputDir.toPath());
}
System.out.println("Removing foreign platform libs ...");
Set extensions = new HashSet<>();
String libExtension;
switch (config.platform) {
case Windows64:
extensions.add(".dylib");
extensions.add(".dylib.git");
extensions.add(".dylib.sha1");
extensions.add(".so");
extensions.add(".so.git");
extensions.add(".so.sha1");
libExtension = ".dll";
break;
case Linux64:
extensions.add(".dll");
extensions.add(".dll.git");
extensions.add(".dll.sha1");
extensions.add(".dylib");
extensions.add(".dylib.git");
extensions.add(".dylib.sha1");
libExtension = ".so";
break;
case MacOS:
extensions.add(".dll");
extensions.add(".dll.git");
extensions.add(".dll.sha1");
extensions.add(".so");
extensions.add(".so.git");
extensions.add(".so.sha1");
libExtension = ".dylib";
break;
default:
throw new IllegalStateException();
}
// let's remove any shared libs not used on the platform, e.g. libGDX/LWJGL natives
for (String classpath : config.removePlatformLibs) {
File jar = new File(output.resourcesFolder, new File(classpath).getName());
File jarDir = new File(output.resourcesFolder, jar.getName() + ".tmp");
if (config.verbose) {
if (jar.isDirectory()) {
System.out.println(" # JAR '" + jar.getName() + "' is a directory");
} else {
System.out.println(" # Unpacking '" + jar.getName() + "' ...");
}
}
if (!jar.isDirectory()) {
ArchiveUtils.extractArchive(jar.toPath(), jarDir.toPath());
} else {
jarDir = jar; // run in-place for directories
}
File[] files = jarDir.listFiles();
if (files != null) {
for (File file : files) {
boolean removed = false;
if (removePlatformLibsFileFilter.test(file)) {
if (config.verbose) {
System.out.println(" # Removing '" + file.getPath() + "' (filtered)");
}
Files.deleteIfExists(file.toPath());
removed = true;
}
if (!removed) {
for (String extension : extensions) {
if (file.getName().endsWith(extension)) {
if (config.verbose) {
System.out.println(" # Removing '" + file.getPath() + "'");
}
Files.deleteIfExists(file.toPath());
removed = true;
break;
}
}
}
if (!removed && extractLibs) {
if (file.getName().endsWith(libExtension)) {
if (config.verbose) {
System.out.println(" # Extracting '" + file.getPath() + "'");
}
File target = new File(libsOutputDir, file.getName());
Files.copy(file.toPath(), target.toPath(), StandardCopyOption.COPY_ATTRIBUTES);
Files.deleteIfExists(file.toPath());
}
}
}
}
if (!jar.isDirectory()) {
if (config.verbose) {
System.out.println(" # Repacking '" + jar.getName() + "' ...");
}
createZipFileFromDirectory(config, jar, jarDir);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy