All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.badlogicgames.packr.PackrReduce Maven / Gradle / Ivy

Go to download

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.

There is a newer version: 2.7.0
Show newest version
/*
 * 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