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

dev.jbang.cli.Export Maven / Gradle / Ivy

There is a newer version: 0.121.0
Show newest version
package dev.jbang.cli;

import static dev.jbang.Settings.CP_SEPARATOR;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.stream.Collectors;

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

import dev.jbang.catalog.Alias;
import dev.jbang.catalog.CatalogUtil;
import dev.jbang.dependencies.ArtifactInfo;
import dev.jbang.dependencies.ArtifactResolver;
import dev.jbang.dependencies.MavenCoordinate;
import dev.jbang.source.BuildContext;
import dev.jbang.source.Project;
import dev.jbang.source.ProjectBuilder;
import dev.jbang.source.ResourceRef;
import dev.jbang.source.resolvers.AliasResourceResolver;
import dev.jbang.util.JarUtil;
import dev.jbang.util.JavaUtil;
import dev.jbang.util.ModuleUtil;
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, ExportFatjar.class, ExportJlink.class })
public class Export {
}

abstract class BaseExportCommand extends BaseCommand {

	@CommandLine.Mixin
	ExportMixin exportMixin;

	protected Manifest createManifest(String newPath) {
		// Create a Manifest with a Class-Path option
		Manifest mf = new Manifest();
		// without MANIFEST_VERSION nothing is saved.
		mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
		mf.getMainAttributes().putValue(Attributes.Name.CLASS_PATH.toString(), newPath);
		return mf;
	}

	@Override
	public Integer doCall() throws IOException {
		exportMixin.validate();
		ProjectBuilder pb = createProjectBuilder(exportMixin);
		Project prj = pb.build(exportMixin.scriptMixin.scriptOrFile);
		BuildContext ctx = BuildContext.forProject(prj);
		Project.codeBuilder(ctx).build();
		if (prj.getResourceRef() instanceof AliasResourceResolver.AliasedResourceRef) {
			Alias alias = ((AliasResourceResolver.AliasedResourceRef) prj.getResourceRef()).getAlias();
			if (prj.getMainClass() == null) {
				prj.setMainClass(alias.mainClass);
			}
		}
		return apply(ctx);
	}

	abstract int apply(BuildContext ctx) throws IOException;

	protected ProjectBuilder createProjectBuilder(ExportMixin exportMixin) {
		return Project
						.builder()
						.setProperties(exportMixin.dependencyInfoMixin.getProperties())
						.additionalDependencies(exportMixin.dependencyInfoMixin.getDependencies())
						.additionalRepositories(exportMixin.dependencyInfoMixin.getRepositories())
						.additionalClasspaths(exportMixin.dependencyInfoMixin.getClasspaths())
						.additionalSources(exportMixin.scriptMixin.sources)
						.additionalResources(exportMixin.scriptMixin.resources)
						.forceType(exportMixin.scriptMixin.forceType)
						.catalog(exportMixin.scriptMixin.catalog)
						.javaVersion(exportMixin.buildMixin.javaVersion)
						.mainClass(exportMixin.buildMixin.main)
						.moduleName(exportMixin.buildMixin.module)
						.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")
class ExportLocal extends BaseExportCommand {

	@Override
	int apply(BuildContext ctx) throws IOException {
		// Copy the JAR
		Path source = ctx.getJarFile();
		Path outputPath = getJarOutputPath();
		if (outputPath.toFile().exists()) {
			if (exportMixin.force) {
				Util.deletePath(outputPath, false);
			} else {
				Util.errorMsg("Cannot export as " + outputPath + " already exists. Use --force to overwrite.");
				return EXIT_INVALID_INPUT;
			}
		} else {
			Util.mkdirs(outputPath.getParent());
		}
		Files.copy(source, outputPath);

		// Update the JAR's MANIFEST.MF Class-Path to point to
		// its dependencies
		Project prj = ctx.getProject();
		String newPath = ctx.resolveClassPath().getManifestPath();
		if (!newPath.isEmpty()) {
			Util.infoMsg("Updating jar...");
			String javaVersion = exportMixin.buildMixin.javaVersion != null
					? exportMixin.buildMixin.javaVersion
					: prj.getJavaVersion();
			JarUtil.updateJar(outputPath, createManifest(newPath), prj.getMainClass(), javaVersion);
		}

		Util.infoMsg("Exported to " + outputPath);
		return EXIT_OK;
	}
}

@Command(name = "portable", description = "Exports jar together with dependencies in way that makes it portable")
class ExportPortable extends BaseExportCommand {

	public static final String LIB = "lib";

	@Override
	int apply(BuildContext ctx) throws IOException {
		// Copy the JAR
		Path source = ctx.getJarFile();
		Path outputPath = getJarOutputPath();
		if (outputPath.toFile().exists()) {
			if (exportMixin.force) {
				Util.deletePath(outputPath, false);
			} else {
				Util.errorMsg("Cannot export as " + outputPath + " already exists. Use --force to overwrite.");
				return EXIT_INVALID_INPUT;
			}
		} else {
			Util.mkdirs(outputPath.getParent());
		}

		Files.copy(source, outputPath);
		Project prj = ctx.getProject();
		List deps = ctx.resolveClassPath().getArtifacts();
		if (!deps.isEmpty()) {
			// Copy dependencies to "./lib" dir
			Path libDir = outputPath.getParent().resolve(LIB);
			Util.mkdirs(libDir);
			StringBuilder newPath = new StringBuilder();
			for (ArtifactInfo dep : deps) {
				if (exportMixin.force) {
					Files.copy(dep.getFile(), libDir.resolve(dep.getFile().getFileName()),
							StandardCopyOption.REPLACE_EXISTING);
				} else {
					Files.copy(dep.getFile(), libDir.resolve(dep.getFile().getFileName()));
				}
				newPath.append(" " + LIB + "/" + dep.getFile().getFileName());
			}

			Util.infoMsg("Updating jar...");
			String javaVersion = exportMixin.buildMixin.javaVersion != null
					? exportMixin.buildMixin.javaVersion
					: prj.getJavaVersion();
			JarUtil.updateJar(outputPath, createManifest(newPath.toString()), prj.getMainClass(), javaVersion);
		}
		Util.infoMsg("Exported to " + outputPath);
		return EXIT_OK;
	}
}

@Command(name = "mavenrepo", description = "Exports directory that can be used to publish as a maven repository")
class ExportMavenPublish extends BaseExportCommand {

	@CommandLine.Option(names = { "--group", "-g" }, description = "The group ID to use for the generated POM.")
	String group;

	@CommandLine.Option(names = { "--artifact", "-a" }, description = "The artifact ID to use for the generated POM.")
	String artifact;

	@CommandLine.Option(names = { "--version", "-v" }, description = "The version to use for the generated POM.")
	String version;

	@Override
	int apply(BuildContext ctx) throws IOException {
		Path outputPath = exportMixin.outputFile;

		if (outputPath == null) {
			outputPath = ArtifactResolver.getLocalMavenRepo();
		}
		// Copy the JAR
		Path source = ctx.getJarFile();

		if (!outputPath.toFile().isDirectory()) {
			if (outputPath.toFile().exists()) {
				Util.errorMsg("Cannot export as maven repository as " + outputPath + " is not a directory.");
				return EXIT_INVALID_INPUT;
			}
			if (exportMixin.force) {
				Util.mkdirs(outputPath);
			} else {
				Util.errorMsg("Cannot export as " + outputPath + " does not exist. Use --force to create.");
				return EXIT_INVALID_INPUT;
			}
		}

		Project prj = ctx.getProject();
		if (prj.getGav().isPresent()) {
			MavenCoordinate coord = MavenCoordinate.fromString(prj.getGav().get()).withVersion();
			if (group == null) {
				group = coord.getGroupId();
			}
			if (artifact == null) {
				artifact = coord.getArtifactId();
			}
			if (version == null) {
				version = coord.getVersion();
			}
		}

		if (group == null) {
			Util.errorMsg(
					"Cannot export as maven repository as no group specified. Add --group= and run again.");
			return EXIT_INVALID_INPUT;
		}
		Path groupdir = outputPath.resolve(Paths.get(group.replace(".", "/")));

		artifact = artifact != null ? artifact
				: Util.getBaseName(prj.getResourceRef().getFile().getFileName().toString());
		Path artifactDir = groupdir.resolve(artifact);

		version = version != null ? version : MavenCoordinate.DEFAULT_VERSION;
		Path versionDir = artifactDir.resolve(version);

		String suffix = source	.getFileName()
								.toString()
								.substring(source.getFileName().toString().lastIndexOf("."));
		Path artifactFile = versionDir.resolve(artifact + "-" + version + suffix);

		artifactFile.getParent().toFile().mkdirs();

		if (artifactFile.toFile().exists()) {
			if (exportMixin.force) {
				artifactFile.toFile().delete();
			} else {
				Util.errorMsg("Cannot export as " + artifactFile + " already exists. Use --force to overwrite.");
				return EXIT_INVALID_INPUT;
			}
		}
		Util.infoMsg("Writing " + artifactFile);
		Files.copy(source, artifactFile);

		// generate pom.xml ... if jar could technically just copy from the jar ...but
		// not possible when native thus for now just regenerate it
		Template pomTemplate = TemplateEngine	.instance()
												.getTemplate(ResourceRef.forResource("classpath:/pom.qute.xml"));

		Path pomPath = versionDir.resolve(artifact + "-" + version + ".pom");
		if (pomTemplate == null) {
			// ignore
			Util.warnMsg("Could not locate pom.xml template");
		} else {

			String pomfile = pomTemplate
										.data("baseName",
												Util.getBaseName(
														prj.getResourceRef().getFile().getFileName().toString()))
										.data("group", group)
										.data("artifact", artifact)
										.data("version", version)
										.data("description", prj.getDescription().orElse(""))
										.data("dependencies", ctx.resolveClassPath().getArtifacts())
										.render();
			Util.infoMsg("Writing " + pomPath);
			Util.writeString(pomPath, pomfile);

		}

		Util.infoMsg("Exported to " + outputPath);
		return EXIT_OK;
	}
}

@Command(name = "native", description = "Exports native executable")
class ExportNative extends BaseExportCommand {

	@Override
	int apply(BuildContext ctx) throws IOException {
		// Copy the native binary
		Path source = ctx.getNativeImageFile();
		Path outputPath = getNativeOutputPath();
		if (outputPath.toFile().exists()) {
			if (exportMixin.force) {
				Util.deletePath(outputPath, false);
			} else {
				Util.errorMsg("Cannot export as " + outputPath + " already exists. Use --force to overwrite.");
				return EXIT_INVALID_INPUT;
			}
		} else {
			Util.mkdirs(outputPath.getParent());
		}
		Files.copy(source, outputPath);

		Util.infoMsg("Exported to " + outputPath);
		return EXIT_OK;
	}

	@Override
	protected ProjectBuilder createProjectBuilder(ExportMixin exportMixin) {
		ProjectBuilder pb = super.createProjectBuilder(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(BuildContext ctx) throws IOException {
		// Copy the native binary
		Path source = ctx.getJarFile();
		Path outputPath = getFatjarOutputPath();
		if (outputPath.toFile().exists()) {
			if (exportMixin.force) {
				Util.deletePath(outputPath, false);
			} else {
				Util.errorMsg("Cannot export as " + outputPath + " already exists. Use --force to overwrite.");
				return EXIT_INVALID_INPUT;
			}
		} else {
			Util.mkdirs(outputPath.getParent());
		}

		Project prj = ctx.getProject();
		List deps = ctx.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);
				}
				JarUtil.createJar(outputPath, tmpDir, null, prj.getMainClass(), prj.getJavaVersion());
			} 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;
	}
}

@Command(name = "jlink", description = "Exports a minimized JDK distribution")
class ExportJlink extends BaseExportCommand {

	@CommandLine.Parameters(index = "1..*", arity = "0..*", description = "Parameters to pass on to the jlink command")
	public List params = new ArrayList<>();

	@Override
	protected ProjectBuilder createProjectBuilder(ExportMixin exportMixin) {
		ProjectBuilder pb = super.createProjectBuilder(exportMixin);
		pb.moduleName("");
		return pb;
	}

	@Override
	int apply(BuildContext ctx) throws IOException {
		Project prj = ctx.getProject();
		List artifacts = ctx.resolveClassPath().getArtifacts();
		List nonMods = artifacts
												.stream()
												.filter(a -> !ModuleUtil.isModule(a.getFile()))
												.collect(Collectors.toList());
		if (!nonMods.isEmpty()) {
			String lst = nonMods
								.stream()
								.map(a -> a.getCoordinate().toCanonicalForm())
								.collect(Collectors.joining(", "));
			Util.warnMsg("Export might fail because some dependencies are not full modules: " + lst);
		}

		Path outputPath = getJlinkOutputPath();
		String relativeOP;
		if (Util.getCwd().relativize(outputPath).isAbsolute()) {
			relativeOP = outputPath.toString();
		} else {
			relativeOP = "." + File.separator + Util.getCwd().relativize(outputPath);
		}
		if (outputPath.toFile().exists()) {
			if (exportMixin.force) {
				Util.deletePath(outputPath, false);
			} else {
				Util.errorMsg("Cannot export as " + relativeOP + " already exists. Use --force to overwrite.");
				return EXIT_INVALID_INPUT;
			}
		}

		String jlinkCmd = JavaUtil.resolveInJavaHome("jlink", null);
		String modMain = ModuleUtil.getModuleMain(prj);
		List cps = artifacts.stream().map(a -> a.getFile().toString()).collect(Collectors.toList());
		List cp = new ArrayList<>(artifacts.size() + 1);
		if (ctx.getJarFile() != null && !cps.contains(ctx.getJarFile().toString())) {
			cp.add(ctx.getJarFile().toString());
		}
		cp.addAll(cps);

		List args = new ArrayList<>();
		args.add(jlinkCmd);
		args.add("--output");
		args.add(outputPath.toString());
		args.add("-p");
		args.add(String.join(CP_SEPARATOR, cp));
		args.add("--add-modules");
		args.add(ModuleUtil.getModuleName(prj));
		String launcherName = null;
		if (modMain != null) {
			launcherName = CatalogUtil.nameFromRef(exportMixin.scriptMixin.scriptOrFile);
			args.add("--launcher");
			args.add(launcherName + "=" + modMain);
		} else {
			Util.warnMsg(
					"No launcher will be generated because no main class is defined. Use '--main' to set a main class");
		}
		args.addAll(params);

		Util.verboseMsg("Run: " + String.join(" ", args));
		String out = Util.runCommand(args.toArray(new String[] {}));
		if (out == null) {
			Util.errorMsg("Unable to export Jdk distribution.");
			return EXIT_GENERIC_ERROR;
		}

		Util.infoMsg("Exported to " + relativeOP);
		if (modMain != null) {
			Util.infoMsg("A launcher has been created which you can run using: " + relativeOP + "/bin/" + launcherName);
		}
		return EXIT_OK;
	}

	private Path getJlinkOutputPath() {
		Path outputPath = exportMixin.getOutputPath("-jlink");
		return outputPath;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy