Skip to content

Commit

Permalink
feat: implemented first version of export fatjar
Browse files Browse the repository at this point in the history
Fixes #1501
  • Loading branch information
quintesse committed Feb 8, 2023
1 parent 9ba1084 commit 53d1e6c
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 37 deletions.
101 changes: 97 additions & 4 deletions src/main/java/dev/jbang/cli/Export.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import static dev.jbang.util.JavaUtil.resolveInJavaHome;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand All @@ -12,20 +15,25 @@
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;

import dev.jbang.Settings;
import dev.jbang.dependencies.ArtifactInfo;
import dev.jbang.dependencies.MavenCoordinate;
import dev.jbang.source.Project;
import dev.jbang.source.ProjectBuilder;
import dev.jbang.util.JarUtil;
import dev.jbang.util.TemplateEngine;
import dev.jbang.util.UnpackUtil;
import dev.jbang.util.Util;

import io.quarkus.qute.Template;
import picocli.CommandLine;
import picocli.CommandLine.Command;

@Command(name = "export", description = "Export the result of a build.", subcommands = { ExportPortable.class,
ExportLocal.class, ExportMavenPublish.class, ExportNative.class })
ExportLocal.class, ExportMavenPublish.class, ExportNative.class, ExportFatjar.class })
public class Export {
}

Expand Down Expand Up @@ -95,6 +103,15 @@ protected ProjectBuilder createProjectBuilder(ExportMixin exportMixin) {
.mainClass(exportMixin.buildMixin.main)
.compileOptions(exportMixin.buildMixin.compileOptions);
}

Path getJarOutputPath() {
Path outputPath = exportMixin.getOutputPath("");
// Ensure the file ends in `.jar`
if (!outputPath.toString().endsWith(".jar")) {
outputPath = Paths.get(outputPath + ".jar");
}
return outputPath;
}
}

@Command(name = "local", description = "Exports jar with classpath referring to local machine dependent locations")
Expand All @@ -104,7 +121,7 @@ class ExportLocal extends BaseExportCommand {
int apply(Project prj, ProjectBuilder pb) throws IOException {
// Copy the JAR
Path source = prj.getJarFile();
Path outputPath = exportMixin.getJarOutputPath();
Path outputPath = getJarOutputPath();
if (outputPath.toFile().exists()) {
if (exportMixin.force) {
outputPath.toFile().delete();
Expand Down Expand Up @@ -141,7 +158,7 @@ class ExportPortable extends BaseExportCommand {
int apply(Project prj, ProjectBuilder pb) throws IOException {
// Copy the JAR
Path source = prj.getJarFile();
Path outputPath = exportMixin.getJarOutputPath();
Path outputPath = getJarOutputPath();
if (outputPath.toFile().exists()) {
if (exportMixin.force) {
outputPath.toFile().delete();
Expand Down Expand Up @@ -292,7 +309,7 @@ class ExportNative extends BaseExportCommand {
int apply(Project prj, ProjectBuilder pb) throws IOException {
// Copy the native binary
Path source = prj.getNativeImageFile();
Path outputPath = exportMixin.getNativeOutputPath();
Path outputPath = getNativeOutputPath();
if (outputPath.toFile().exists()) {
if (exportMixin.force) {
outputPath.toFile().delete();
Expand All @@ -313,4 +330,80 @@ protected ProjectBuilder createProjectBuilder(ExportMixin exportMixin) {
pb.nativeImage(true);
return pb;
}

Path getNativeOutputPath() {
Path outputPath = exportMixin.getOutputPath("");
// Ensure that on Windows the file ends in `.exe`
if (Util.isWindows() && !outputPath.toString().endsWith(".exe")) {
outputPath = Paths.get(outputPath + ".exe");
}
return outputPath;
}
}

@Command(name = "fatjar", description = "Exports an executable jar with all necessary dependencies included inside")
class ExportFatjar extends BaseExportCommand {

@Override
int apply(Project prj, ProjectBuilder pb) throws IOException {
// Copy the native binary
Path source = prj.getJarFile();
Path outputPath = getFatjarOutputPath();
if (outputPath.toFile().exists()) {
if (exportMixin.force) {
outputPath.toFile().delete();
} else {
Util.warnMsg("Cannot export as " + outputPath + " already exists. Use --force to overwrite.");
return EXIT_INVALID_INPUT;
}
}

List<ArtifactInfo> deps = prj.resolveClassPath().getArtifacts();
if (!deps.isEmpty()) {
// Extract main jar and all dependencies to a temp dir
Path tmpDir = Files.createTempDirectory("fatjar");
try {
Util.verboseMsg("Unpacking main jar: " + source);
UnpackUtil.unzip(source, tmpDir, false, null, ExportFatjar::handleExistingFile);
for (ArtifactInfo dep : deps) {
Util.verboseMsg("Unpacking artifact: " + dep);
UnpackUtil.unzip(dep.getFile(), tmpDir, false, null, ExportFatjar::handleExistingFile);
}
try (OutputStream out = Files.newOutputStream(outputPath)) {
JarUtil.jar(out, tmpDir.toFile().listFiles());
}
} finally {
Util.deletePath(tmpDir, true);
}
} else {
// No dependencies so we simply copy the main jar
Files.copy(source, outputPath);
}

Util.infoMsg("Exported to " + outputPath);
Util.infoMsg("This is an experimental feature and might not to work for certain applications!");
Util.infoMsg("Help us improve by reporting any issue you find at https://github.com/jbangdev/jbang/issues");
return EXIT_OK;
}

public static void handleExistingFile(ZipFile zipFile, ZipArchiveEntry zipEntry, Path outFile) throws IOException {
if (zipEntry.getName().startsWith("META-INF/services/")) {
Util.verboseMsg("Merging service files: " + zipEntry.getName());
try (ReadableByteChannel readableByteChannel = Channels.newChannel(zipFile.getInputStream(zipEntry));
FileOutputStream fileOutputStream = new FileOutputStream(outFile.toFile(), true)) {
fileOutputStream.getChannel().transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
}
} else {
Util.verboseMsg("Skipping duplicate file: " + zipEntry.getName());
}
}

private Path getFatjarOutputPath() {
Path outputPath = exportMixin.getOutputPath("-fatjar");
// Ensure the file ends in `.jar`
if (!outputPath.toString().endsWith(".jar")) {
outputPath = Paths.get(outputPath + ".jar");
}
return outputPath;
}
}
22 changes: 2 additions & 20 deletions src/main/java/dev/jbang/cli/ExportMixin.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,33 +35,15 @@ public class ExportMixin {
public ExportMixin() {
}

Path getJarOutputPath() {
Path outputPath = getOutputPath();
// Ensure the file ends in `.jar`
if (!outputPath.toString().endsWith(".jar")) {
outputPath = Paths.get(outputPath + ".jar");
}
return outputPath;
}

Path getNativeOutputPath() {
Path outputPath = getOutputPath();
// Ensure that on Windows the file ends in `.exe`
if (Util.isWindows() && !outputPath.toString().endsWith(".exe")) {
outputPath = Paths.get(outputPath + ".exe");
}
return outputPath;
}

private Path getOutputPath() {
Path getOutputPath(String postFix) {
// Determine the output file location and name
Path cwd = Util.getCwd();
Path outputPath;
if (outputFile != null) {
outputPath = outputFile;
} else {
String outName = CatalogUtil.nameFromRef(scriptMixin.scriptOrFile);
outputPath = Paths.get(outName);
outputPath = Paths.get(outName + postFix);
}
outputPath = cwd.resolve(outputPath);
return outputPath;
Expand Down
42 changes: 29 additions & 13 deletions src/main/java/dev/jbang/util/UnpackUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Enumeration;
import java.util.LinkedHashSet;
Expand All @@ -26,7 +27,7 @@ public static void unpackEditor(Path archive, Path outputDir) throws IOException
Path selectFolder = null; // Util.isMac() ? Paths.get("Contents/Home") : null;
boolean stripRootFolder = Util.isMac();
if (name.endsWith(".zip")) {
unzip(archive, outputDir, stripRootFolder, selectFolder);
unzip(archive, outputDir, stripRootFolder, selectFolder, UnpackUtil::defaultZipEntryCopy);
} else if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
untargz(archive, outputDir, false, selectFolder);
}
Expand All @@ -36,7 +37,7 @@ public static void unpackJdk(Path archive, Path outputDir) throws IOException {
String name = archive.toString().toLowerCase(Locale.ENGLISH);
Path selectFolder = Util.isMac() ? Paths.get("Contents/Home") : null;
if (name.endsWith(".zip")) {
unzip(archive, outputDir, true, selectFolder);
unzip(archive, outputDir, true, selectFolder, UnpackUtil::defaultZipEntryCopy);
} else if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
untargz(archive, outputDir, true, selectFolder);
}
Expand All @@ -53,14 +54,17 @@ public static void unpack(Path archive, Path outputDir, boolean stripRootFolder)
public static void unpack(Path archive, Path outputDir, boolean stripRootFolder, Path selectFolder)
throws IOException {
String name = archive.toString().toLowerCase(Locale.ENGLISH);
if (name.endsWith(".zip")) {
unzip(archive, outputDir, stripRootFolder, selectFolder);
if (name.endsWith(".zip") || name.endsWith(".jar")) {
unzip(archive, outputDir, stripRootFolder, selectFolder, UnpackUtil::defaultZipEntryCopy);
} else if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
untargz(archive, outputDir, stripRootFolder, selectFolder);
} else {
throw new IllegalArgumentException("Unsupported archive format: " + Util.extension(archive.toString()));
}
}

public static void unzip(Path zip, Path outputDir, boolean stripRootFolder, Path selectFolder) throws IOException {
public static void unzip(Path zip, Path outputDir, boolean stripRootFolder, Path selectFolder,
ExistingZipFileHandler onExisting) throws IOException {
try (ZipFile zipFile = new ZipFile(zip.toFile())) {
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
while (entries.hasMoreElements()) {
Expand Down Expand Up @@ -92,19 +96,31 @@ public static void unzip(Path zip, Path outputDir, boolean stripRootFolder, Path
if (!Files.isDirectory(entry.getParent())) {
Files.createDirectories(entry.getParent());
}
try (InputStream zis = zipFile.getInputStream(zipEntry)) {
Files.copy(zis, entry);
}
int mode = zipEntry.getUnixMode();
if (mode != 0 && !Util.isWindows()) {
Set<PosixFilePermission> permissions = PosixFilePermissionSupport.toPosixFilePermissions(mode);
Files.setPosixFilePermissions(entry, permissions);
if (Files.isRegularFile(entry)) {
onExisting.handle(zipFile, zipEntry, entry);
} else {
defaultZipEntryCopy(zipFile, zipEntry, entry);
}
}
}
}
}

public interface ExistingZipFileHandler {
void handle(ZipFile zipFile, ZipArchiveEntry zipEntry, Path outFile) throws IOException;
}

public static void defaultZipEntryCopy(ZipFile zipFile, ZipArchiveEntry zipEntry, Path outFile) throws IOException {
try (InputStream zis = zipFile.getInputStream(zipEntry)) {
Files.copy(zis, outFile, StandardCopyOption.REPLACE_EXISTING);
}
int mode = zipEntry.getUnixMode();
if (mode != 0 && !Util.isWindows()) {
Set<PosixFilePermission> permissions = PosixFilePermissionSupport.toPosixFilePermissions(mode);
Files.setPosixFilePermissions(outFile, permissions);
}
}

public static void untargz(Path targz, Path outputDir, boolean stripRootFolder, Path selectFolder)
throws IOException {
try (TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(
Expand Down Expand Up @@ -135,7 +151,7 @@ public static void untargz(Path targz, Path outputDir, boolean stripRootFolder,
if (!Files.isDirectory(entry.getParent())) {
Files.createDirectories(entry.getParent());
}
Files.copy(tarArchiveInputStream, entry);
Files.copy(tarArchiveInputStream, entry, StandardCopyOption.REPLACE_EXISTING);
int mode = targzEntry.getMode();
if (mode != 0 && !Util.isWindows()) {
Set<PosixFilePermission> permissions = PosixFilePermissionSupport.toPosixFilePermissions(mode);
Expand Down

0 comments on commit 53d1e6c

Please sign in to comment.