@SuppressWarnings("UseOfSystemOutOrSystemErr")
  public static Patch create(PatchSpec spec, File patchFile, Path cacheDir) throws IOException {
    LOG.info("Creating the patch file '" + patchFile + "'...");
    System.out.println("Creating the patch file '" + patchFile + "'...");

    var patchInfo = new Patch(spec);

    LOG.info("Packing entries...");
    System.out.println("Packing entries...");

    List<PatchAction> actions = patchInfo.getActions();
    File olderDir = new File(spec.getOldFolder());
    File newerDir = new File(spec.getNewFolder());
    ExecutorService executor = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors() - 1, 1));
    Map<PatchAction, Future<Path>> tasks = new ConcurrentHashMap<>();

    for (int i = 0; i < actions.size(); i++) {
      PatchAction action = actions.get(i);
      if (action instanceof UpdateAction && !action.isCritical()) {
        int _i = i;
        tasks.put(action, executor.submit(() -> {
          Path cached = null;
          if (cacheDir != null) {
            String cachedName = getCachedFileName((UpdateAction)action, newerDir);
            if (cachedName != null) {
              cached = cacheDir.resolve(cachedName);
              if (Files.exists(cached) && Files.isRegularFile(cached)) {
                LOG.info("Reusing diff for " + action.getPath() + " : " + cachedName);
                return cached;
              }
            }
          }
          Path temp = Utils.getTempFile("diff_" + _i).toPath();
          try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(temp))) {
            out.setLevel(0);
            action.buildPatchFile(olderDir, newerDir, out);
          }
          if (cached != null) {
            Files.createDirectories(cached.getParent());
            Files.copy(temp, cached);
            LOG.info("Caching diff for " + action.getPath() + " : " + cached.getFileName());
          }
          return temp;
        }));
      }
    }

    try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(patchFile))) {
      out.setLevel(9);

      LOG.info("Packing " + PATCH_INFO_FILE_NAME);
      out.putNextEntry(new ZipEntry(PATCH_INFO_FILE_NAME));
      patchInfo.write(out);
      out.closeEntry();

      for (PatchAction action : actions) {
        LOG.info("Packing " + action.getPath());
        Future<Path> task = tasks.get(action);
        if (task == null) {
          action.buildPatchFile(olderDir, newerDir, out);
        }
        else {
          try {
            Path temp = task.get();
            try (ZipInputStream in = new ZipInputStream(Files.newInputStream(temp))) {
              ZipEntry entry;
              while ((entry = in.getNextEntry()) != null) {
                out.putNextEntry(new ZipEntry(entry.getName()));
                Utils.copyStream(in, out);
                out.closeEntry();
              }
            }
          }
          catch (InterruptedException e) { throw new IOException(e); }
          catch (ExecutionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof IOException) throw (IOException)cause;
            if (cause instanceof RuntimeException) throw (RuntimeException)cause;
            if (cause instanceof Error) throw (Error)cause;
            throw new RuntimeException(e);
          }
        }
      }
    }

    executor.shutdown();

    return patchInfo;
  }
@SuppressWarnings("UseOfSystemOutOrSystemErr")
  public static Patch create(PatchSpec spec, File patchFile, Path cacheDir)