diff --git a/make/CompileJavaModules.gmk b/make/CompileJavaModules.gmk --- a/make/CompileJavaModules.gmk +++ b/make/CompileJavaModules.gmk @@ -380,6 +380,13 @@ ################################################################################ +jdk.incubator.jpackage_COPY += .gif .png .txt .spec .script .prerm .preinst .postrm .postinst .list .sh \ + .desktop .copyright .control .plist .template .icns .scpt .entitlements .wxs .wxl .wxi .ico .bmp + +jdk.incubator.jpackage_CLEAN += .properties + +################################################################################ + jdk.jconsole_COPY += .gif .png jdk.jconsole_CLEAN_FILES += $(wildcard \ diff --git a/make/common/Modules.gmk b/make/common/Modules.gmk --- a/make/common/Modules.gmk +++ b/make/common/Modules.gmk @@ -128,6 +128,7 @@ JRE_TOOL_MODULES += \ jdk.jdwp.agent \ + jdk.incubator.jpackage \ jdk.pack \ jdk.scripting.nashorn.shell \ # @@ -149,6 +150,7 @@ jdk.editpad \ jdk.hotspot.agent \ jdk.httpserver \ + jdk.incubator.jpackage \ jdk.jartool \ jdk.javadoc \ jdk.jcmd \ @@ -243,6 +245,13 @@ endif ################################################################################ +# jpackage is only on windows, macosx, and linux + +ifeq ($(call isTargetOs, windows macosx linux), false) + MODULES_FILTER += jdk.incubator.jpackage +endif + +################################################################################ # Module list macros # Use append so that the custom extension may add to these variables diff --git a/make/common/NativeCompilation.gmk b/make/common/NativeCompilation.gmk --- a/make/common/NativeCompilation.gmk +++ b/make/common/NativeCompilation.gmk @@ -397,6 +397,7 @@ # ARFLAGS the archiver flags to be used # OBJECT_DIR the directory where we store the object files # OUTPUT_DIR the directory where the resulting binary is put +# SYMBOLS_DIR the directory where the debug symbols are put, defaults to OUTPUT_DIR # INCLUDES only pick source from these directories # EXCLUDES do not pick source from these directories # INCLUDE_FILES only compile exactly these files! @@ -533,8 +534,6 @@ $$(call SetIfEmpty, $1_SYSROOT_CFLAGS, $$($$($1_TOOLCHAIN)_SYSROOT_CFLAGS)) $$(call SetIfEmpty, $1_SYSROOT_LDFLAGS, $$($$($1_TOOLCHAIN)_SYSROOT_LDFLAGS)) - # Make sure the dirs exist. - $$(call MakeDir, $$($1_OBJECT_DIR) $$($1_OUTPUT_DIR)) $$(foreach d, $$($1_SRC), $$(if $$(wildcard $$d), , \ $$(error SRC specified to SetupNativeCompilation $1 contains missing directory $$d))) @@ -911,30 +910,31 @@ ifeq ($$($1_COPY_DEBUG_SYMBOLS), true) ifneq ($$($1_DEBUG_SYMBOLS), false) + $$(call SetIfEmpty, $1_SYMBOLS_DIR, $$($1_OUTPUT_DIR)) # Only copy debug symbols for dynamic libraries and programs. ifneq ($$($1_TYPE), STATIC_LIBRARY) # Generate debuginfo files. ifeq ($(call isTargetOs, windows), true) - $1_EXTRA_LDFLAGS += -debug "-pdb:$$($1_OUTPUT_DIR)/$$($1_NOSUFFIX).pdb" \ - "-map:$$($1_OUTPUT_DIR)/$$($1_NOSUFFIX).map" - $1_DEBUGINFO_FILES := $$($1_OUTPUT_DIR)/$$($1_NOSUFFIX).pdb \ - $$($1_OUTPUT_DIR)/$$($1_NOSUFFIX).map + $1_EXTRA_LDFLAGS += -debug "-pdb:$$($1_SYMBOLS_DIR)/$$($1_NOSUFFIX).pdb" \ + "-map:$$($1_SYMBOLS_DIR)/$$($1_NOSUFFIX).map" + $1_DEBUGINFO_FILES := $$($1_SYMBOLS_DIR)/$$($1_NOSUFFIX).pdb \ + $$($1_SYMBOLS_DIR)/$$($1_NOSUFFIX).map else ifeq ($(call isTargetOs, linux solaris), true) - $1_DEBUGINFO_FILES := $$($1_OUTPUT_DIR)/$$($1_NOSUFFIX).debuginfo + $1_DEBUGINFO_FILES := $$($1_SYMBOLS_DIR)/$$($1_NOSUFFIX).debuginfo # Setup the command line creating debuginfo files, to be run after linking. # It cannot be run separately since it updates the original target file $1_CREATE_DEBUGINFO_CMDS := \ $$($1_OBJCOPY) --only-keep-debug $$($1_TARGET) $$($1_DEBUGINFO_FILES) $$(NEWLINE) \ - $(CD) $$($1_OUTPUT_DIR) && \ + $(CD) $$($1_SYMBOLS_DIR) && \ $$($1_OBJCOPY) --add-gnu-debuglink=$$($1_DEBUGINFO_FILES) $$($1_TARGET) else ifeq ($(call isTargetOs, macosx), true) $1_DEBUGINFO_FILES := \ - $$($1_OUTPUT_DIR)/$$($1_BASENAME).dSYM/Contents/Info.plist \ - $$($1_OUTPUT_DIR)/$$($1_BASENAME).dSYM/Contents/Resources/DWARF/$$($1_BASENAME) + $$($1_SYMBOLS_DIR)/$$($1_BASENAME).dSYM/Contents/Info.plist \ + $$($1_SYMBOLS_DIR)/$$($1_BASENAME).dSYM/Contents/Resources/DWARF/$$($1_BASENAME) $1_CREATE_DEBUGINFO_CMDS := \ - $(DSYMUTIL) --out $$($1_OUTPUT_DIR)/$$($1_BASENAME).dSYM $$($1_TARGET) + $(DSYMUTIL) --out $$($1_SYMBOLS_DIR)/$$($1_BASENAME).dSYM $$($1_TARGET) endif # Since the link rule creates more than one file that we want to track, @@ -956,14 +956,14 @@ $1 += $$($1_DEBUGINFO_FILES) ifeq ($$($1_ZIP_EXTERNAL_DEBUG_SYMBOLS), true) - $1_DEBUGINFO_ZIP := $$($1_OUTPUT_DIR)/$$($1_NOSUFFIX).diz + $1_DEBUGINFO_ZIP := $$($1_SYMBOLS_DIR)/$$($1_NOSUFFIX).diz $1 += $$($1_DEBUGINFO_ZIP) # The dependency on TARGET is needed for debuginfo files # to be rebuilt properly. $$($1_DEBUGINFO_ZIP): $$($1_DEBUGINFO_FILES) $$($1_TARGET) - $(CD) $$($1_OUTPUT_DIR) && \ - $(ZIPEXE) -q -r $$@ $$(subst $$($1_OUTPUT_DIR)/,, $$($1_DEBUGINFO_FILES)) + $(CD) $$($1_SYMBOLS_DIR) && \ + $(ZIPEXE) -q -r $$@ $$(subst $$($1_SYMBOLS_DIR)/,, $$($1_DEBUGINFO_FILES)) endif endif # !STATIC_LIBRARY @@ -999,6 +999,7 @@ $$($1_TARGET): $$($1_TARGET_DEPS) $$(call LogInfo, Building static library $$($1_BASENAME)) + $$(call MakeDir, $$($1_OUTPUT_DIR) $$($1_SYMBOLS_DIR)) $$(call ExecuteWithLog, $$($1_OBJECT_DIR)/$$($1_SAFE_NAME)_link, \ $$($1_AR) $$($1_ARFLAGS) $(AR_OUT_OPTION)$$($1_TARGET) $$($1_ALL_OBJS) \ $$($1_RES)) @@ -1100,7 +1101,9 @@ # Keep as much as possible on one execution line for best performance # on Windows $$(call LogInfo, Linking $$($1_BASENAME)) + $$(call MakeDir, $$($1_OUTPUT_DIR) $$($1_SYMBOLS_DIR)) ifeq ($(call isTargetOs, windows), true) + $$(call ExecuteWithLog, $$($1_OBJECT_DIR)/$$($1_SAFE_NAME)_link, \ $$($1_LD) $$($1_LDFLAGS) $$($1_EXTRA_LDFLAGS) $$($1_SYSROOT_LDFLAGS) \ $(LD_OUT_OPTION)$$($1_TARGET) $$($1_LD_OBJ_ARG) $$($1_RES) $$(GLOBAL_LIBS) \ diff --git a/make/launcher/Launcher-jdk.incubator.jpackage.gmk b/make/launcher/Launcher-jdk.incubator.jpackage.gmk new file mode 100644 --- /dev/null +++ b/make/launcher/Launcher-jdk.incubator.jpackage.gmk @@ -0,0 +1,30 @@ +# +# Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +include LauncherCommon.gmk + +$(eval $(call SetupBuildLauncher, jpackage, \ + MAIN_CLASS := jdk.incubator.jpackage.main.Main, \ +)) diff --git a/make/lib/Lib-jdk.incubator.jpackage.gmk b/make/lib/Lib-jdk.incubator.jpackage.gmk new file mode 100644 --- /dev/null +++ b/make/lib/Lib-jdk.incubator.jpackage.gmk @@ -0,0 +1,140 @@ +# +# Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +include LibCommon.gmk + +################################################################################ + +# Output app launcher library in resources dir, and symbols in the object dir +$(eval $(call SetupJdkLibrary, BUILD_LIB_APPLAUNCHER, \ + NAME := applauncher, \ + OUTPUT_DIR := $(JDK_OUTPUTDIR)/modules/$(MODULE)/jdk/incubator/jpackage/internal/resources, \ + SYMBOLS_DIR := $(SUPPORT_OUTPUTDIR)/native/$(MODULE)/libapplauncher, \ + TOOLCHAIN := TOOLCHAIN_LINK_CXX, \ + OPTIMIZATION := LOW, \ + CFLAGS := $(CXXFLAGS_JDKLIB), \ + CFLAGS_windows := -EHsc -DUNICODE -D_UNICODE, \ + LDFLAGS := $(LDFLAGS_JDKLIB) $(LDFLAGS_CXX_JDK) \ + $(call SET_SHARED_LIBRARY_ORIGIN), \ + LIBS := $(LIBCXX), \ + LIBS_windows := user32.lib shell32.lib advapi32.lib ole32.lib, \ + LIBS_linux := -ldl -lpthread, \ + LIBS_macosx := -ldl -framework Cocoa, \ +)) + +$(BUILD_LIB_APPLAUNCHER): $(call FindLib, java.base, java) + +TARGETS += $(BUILD_LIB_APPLAUNCHER) + +JPACKAGE_APPLAUNCHER_SRC := \ + $(TOPDIR)/src/jdk.incubator.jpackage/$(OPENJDK_TARGET_OS)/native/jpackageapplauncher + +# Output app launcher executable in resources dir, and symbols in the object dir +$(eval $(call SetupJdkExecutable, BUILD_JPACKAGE_APPLAUNCHEREXE, \ + NAME := jpackageapplauncher, \ + OUTPUT_DIR := $(JDK_OUTPUTDIR)/modules/$(MODULE)/jdk/incubator/jpackage/internal/resources, \ + SYMBOLS_DIR := $(SUPPORT_OUTPUTDIR)/native/$(MODULE)/jpackageapplauncher, \ + SRC := $(JPACKAGE_APPLAUNCHER_SRC), \ + TOOLCHAIN := TOOLCHAIN_LINK_CXX, \ + OPTIMIZATION := LOW, \ + CFLAGS := $(CXXFLAGS_JDKEXE), \ + CFLAGS_windows := -EHsc -DLAUNCHERC -DUNICODE -D_UNICODE, \ + LDFLAGS := $(LDFLAGS_JDKEXE), \ + LIBS_macosx := -framework Cocoa, \ + LIBS := $(LIBCXX), \ + LIBS_linux := -ldl, \ + LIBS_windows := user32.lib shell32.lib advapi32.lib, \ +)) + +TARGETS += $(BUILD_JPACKAGE_APPLAUNCHEREXE) + +################################################################################ + +ifeq ($(call isTargetOs, windows), true) + + $(eval $(call SetupJdkLibrary, BUILD_LIB_JPACKAGE, \ + NAME := jpackage, \ + OPTIMIZATION := LOW, \ + CFLAGS := $(CXXFLAGS_JDKLIB), \ + CFLAGS_windows := -EHsc -DUNICODE -D_UNICODE, \ + LDFLAGS := $(LDFLAGS_JDKLIB) $(LDFLAGS_CXX_JDK) \ + $(call SET_SHARED_LIBRARY_ORIGIN), \ + LIBS := $(LIBCXX), \ + LIBS_windows := user32.lib shell32.lib advapi32.lib ole32.lib, \ + )) + + TARGETS += $(BUILD_LIB_JPACKAGE) + + # Build Wix custom action helper + # Output library in resources dir, and symbols in the object dir + $(eval $(call SetupJdkLibrary, BUILD_LIB_WIXHELPER, \ + NAME := wixhelper, \ + OUTPUT_DIR := $(JDK_OUTPUTDIR)/modules/$(MODULE)/jdk/incubator/jpackage/internal/resources, \ + SYMBOLS_DIR := $(SUPPORT_OUTPUTDIR)/native/$(MODULE)/libwixhelper, \ + OPTIMIZATION := LOW, \ + CFLAGS := $(CXXFLAGS_JDKLIB), \ + CFLAGS_windows := -EHsc -DUNICODE -D_UNICODE -MT, \ + LDFLAGS := $(LDFLAGS_JDKLIB) $(LDFLAGS_CXX_JDK), \ + LIBS := $(LIBCXX), \ + LIBS_windows := msi.lib Shlwapi.lib User32.lib, \ + )) + + TARGETS += $(BUILD_LIB_WIXHELPER) + + # Build exe installer wrapper for msi installer + $(eval $(call SetupJdkExecutable, BUILD_JPACKAGE_MSIWRAPPER, \ + NAME := msiwrapper, \ + OUTPUT_DIR := $(JDK_OUTPUTDIR)/modules/$(MODULE)/jdk/incubator/jpackage/internal/resources, \ + SYMBOLS_DIR := $(SUPPORT_OUTPUTDIR)/native/$(MODULE)/msiwrapper, \ + SRC := $(TOPDIR)/src/jdk.incubator.jpackage/$(OPENJDK_TARGET_OS)/native/msiwrapper, \ + EXTRA_FILES := $(addprefix $(TOPDIR)/src/jdk.incubator.jpackage/$(OPENJDK_TARGET_OS)/native/libjpackage/, \ + FileUtils.cpp Log.cpp WinSysInfo.cpp tstrings.cpp WinErrorHandling.cpp ErrorHandling.cpp), \ + CFLAGS := $(CXXFLAGS_JDKEXE) -MT \ + $(addprefix -I$(TOPDIR)/src/jdk.incubator.jpackage/$(OPENJDK_TARGET_OS)/native/, msiwrapper libjpackage), \ + CFLAGS_windows := -EHsc -DUNICODE -D_UNICODE, \ + LDFLAGS := $(LDFLAGS_JDKEXE), \ + LIBS := $(LIBCXX), \ + )) + + TARGETS += $(BUILD_JPACKAGE_MSIWRAPPER) + + # Build non-console version of launcher + $(eval $(call SetupJdkExecutable, BUILD_JPACKAGE_APPLAUNCHERWEXE, \ + NAME := jpackageapplauncherw, \ + OUTPUT_DIR := $(JDK_OUTPUTDIR)/modules/$(MODULE)/jdk/incubator/jpackage/internal/resources, \ + SYMBOLS_DIR := $(SUPPORT_OUTPUTDIR)/native/$(MODULE)/jpackageapplauncherw, \ + SRC := $(JPACKAGE_APPLAUNCHER_SRC), \ + TOOLCHAIN := TOOLCHAIN_LINK_CXX, \ + OPTIMIZATION := LOW, \ + CFLAGS := $(CXXFLAGS_JDKEXE), \ + CFLAGS_windows := -EHsc -DUNICODE -D_UNICODE, \ + LDFLAGS := $(LDFLAGS_JDKEXE), \ + LIBS := $(LIBCXX), \ + LIBS_windows := user32.lib shell32.lib advapi32.lib, \ + )) + + TARGETS += $(BUILD_JPACKAGE_APPLAUNCHERWEXE) + +endif diff --git a/src/java.base/share/classes/module-info.java b/src/java.base/share/classes/module-info.java --- a/src/java.base/share/classes/module-info.java +++ b/src/java.base/share/classes/module-info.java @@ -203,7 +203,8 @@ java.management.rmi, jdk.jartool, jdk.jfr, - jdk.jlink; + jdk.jlink, + jdk.incubator.jpackage; exports jdk.internal.perf to java.management, jdk.management.agent, diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/DesktopIntegration.java b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/DesktopIntegration.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/DesktopIntegration.java @@ -0,0 +1,503 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.awt.image.BufferedImage; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.imageio.ImageIO; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import static jdk.incubator.jpackage.internal.LinuxAppBundler.ICON_PNG; +import static jdk.incubator.jpackage.internal.LinuxAppImageBuilder.DEFAULT_ICON; +import static jdk.incubator.jpackage.internal.OverridableResource.createResource; +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + +/** + * Helper to create files for desktop integration. + */ +final class DesktopIntegration { + + static final String DESKTOP_COMMANDS_INSTALL = "DESKTOP_COMMANDS_INSTALL"; + static final String DESKTOP_COMMANDS_UNINSTALL = "DESKTOP_COMMANDS_UNINSTALL"; + static final String UTILITY_SCRIPTS = "UTILITY_SCRIPTS"; + + DesktopIntegration(PlatformPackage thePackage, + Map params) { + + associations = FileAssociation.fetchFrom(params).stream() + .filter(fa -> !fa.mimeTypes.isEmpty()) + .map(LinuxFileAssociation::new) + .collect(Collectors.toUnmodifiableList()); + + launchers = ADD_LAUNCHERS.fetchFrom(params); + + this.thePackage = thePackage; + + final File customIconFile = ICON_PNG.fetchFrom(params); + + iconResource = createResource(DEFAULT_ICON, params) + .setCategory(I18N.getString("resource.menu-icon")) + .setExternal(customIconFile); + desktopFileResource = createResource("template.desktop", params) + .setCategory(I18N.getString("resource.menu-shortcut-descriptor")) + .setPublicName(APP_NAME.fetchFrom(params) + ".desktop"); + + // XDG recommends to use vendor prefix in desktop file names as xdg + // commands copy files to system directories. + // Package name should be a good prefix. + final String desktopFileName = String.format("%s-%s.desktop", + thePackage.name(), APP_NAME.fetchFrom(params)); + final String mimeInfoFileName = String.format("%s-%s-MimeInfo.xml", + thePackage.name(), APP_NAME.fetchFrom(params)); + + mimeInfoFile = new DesktopFile(mimeInfoFileName); + + if (!associations.isEmpty() || SHORTCUT_HINT.fetchFrom(params) || customIconFile != null) { + // + // Create primary .desktop file if one of conditions is met: + // - there are file associations configured + // - user explicitely requested to create a shortcut + // - custom icon specified + // + desktopFile = new DesktopFile(desktopFileName); + iconFile = new DesktopFile(APP_NAME.fetchFrom(params) + + IOUtils.getSuffix(Path.of(DEFAULT_ICON))); + } else { + desktopFile = null; + iconFile = null; + } + + desktopFileData = Collections.unmodifiableMap( + createDataForDesktopFile(params)); + + nestedIntegrations = launchers.stream().map( + launcherParams -> new DesktopIntegration(thePackage, + launcherParams)).collect(Collectors.toList()); + } + + List requiredPackages() { + return Stream.of(List.of(this), nestedIntegrations).flatMap( + List::stream).map(DesktopIntegration::requiredPackagesSelf).flatMap( + List::stream).distinct().collect(Collectors.toList()); + } + + Map create() throws IOException { + associations.forEach(assoc -> assoc.data.verify()); + + if (iconFile != null) { + // Create application icon file. + iconResource.saveToFile(iconFile.srcPath()); + } + + Map data = new HashMap<>(desktopFileData); + + final ShellCommands shellCommands; + if (desktopFile != null) { + // Create application desktop description file. + createDesktopFile(data); + + // Shell commands will be created only if desktop file + // should be installed. + shellCommands = new ShellCommands(); + } else { + shellCommands = null; + } + + if (!associations.isEmpty()) { + // Create XML file with mime types corresponding to file associations. + createFileAssociationsMimeInfoFile(); + + shellCommands.setFileAssociations(); + + // Create icon files corresponding to file associations + addFileAssociationIconFiles(shellCommands); + } + + // Create shell commands to install/uninstall integration with desktop of the app. + if (shellCommands != null) { + shellCommands.applyTo(data); + } + + boolean needCleanupScripts = !associations.isEmpty(); + + // Take care of additional launchers if there are any. + // Process every additional launcher as the main application launcher. + // Collect shell commands to install/uninstall integration with desktop + // of the additional launchers and append them to the corresponding + // commands of the main launcher. + List installShellCmds = new ArrayList<>(Arrays.asList( + data.get(DESKTOP_COMMANDS_INSTALL))); + List uninstallShellCmds = new ArrayList<>(Arrays.asList( + data.get(DESKTOP_COMMANDS_UNINSTALL))); + for (var integration: nestedIntegrations) { + if (!integration.associations.isEmpty()) { + needCleanupScripts = true; + } + + Map launcherData = integration.create(); + + installShellCmds.add(launcherData.get(DESKTOP_COMMANDS_INSTALL)); + uninstallShellCmds.add(launcherData.get( + DESKTOP_COMMANDS_UNINSTALL)); + } + + data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands( + installShellCmds)); + data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands( + uninstallShellCmds)); + + if (needCleanupScripts) { + // Pull in utils.sh scrips library. + try (InputStream is = OverridableResource.readDefault("utils.sh"); + InputStreamReader isr = new InputStreamReader(is); + BufferedReader reader = new BufferedReader(isr)) { + data.put(UTILITY_SCRIPTS, reader.lines().collect( + Collectors.joining(System.lineSeparator()))); + } + } else { + data.put(UTILITY_SCRIPTS, ""); + } + + return data; + } + + private List requiredPackagesSelf() { + if (desktopFile != null) { + return List.of("xdg-utils"); + } + return Collections.emptyList(); + } + + private Map createDataForDesktopFile( + Map params) { + Map data = new HashMap<>(); + data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params)); + data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params)); + data.put("APPLICATION_ICON", + iconFile != null ? iconFile.installPath().toString() : null); + data.put("DEPLOY_BUNDLE_CATEGORY", MENU_GROUP.fetchFrom(params)); + data.put("APPLICATION_LAUNCHER", + thePackage.installedApplicationLayout().launchersDirectory().resolve( + LinuxAppImageBuilder.getLauncherName(params)).toString()); + + return data; + } + + /** + * Shell commands to integrate something with desktop. + */ + private class ShellCommands { + + ShellCommands() { + registerIconCmds = new ArrayList<>(); + unregisterIconCmds = new ArrayList<>(); + + registerDesktopFileCmd = String.join(" ", "xdg-desktop-menu", + "install", desktopFile.installPath().toString()); + unregisterDesktopFileCmd = String.join(" ", "xdg-desktop-menu", + "uninstall", desktopFile.installPath().toString()); + } + + void setFileAssociations() { + registerFileAssociationsCmd = String.join(" ", "xdg-mime", + "install", + mimeInfoFile.installPath().toString()); + unregisterFileAssociationsCmd = String.join(" ", "xdg-mime", + "uninstall", mimeInfoFile.installPath().toString()); + + // + // Add manual cleanup of system files to get rid of + // the default mime type handlers. + // + // Even after mime type is unregisterd with `xdg-mime uninstall` + // command and desktop file deleted with `xdg-desktop-menu uninstall` + // command, records in + // `/usr/share/applications/defaults.list` (Ubuntu 16) or + // `/usr/local/share/applications/defaults.list` (OracleLinux 7) + // files remain referencing deleted mime time and deleted + // desktop file which makes `xdg-mime query default` output name + // of non-existing desktop file. + // + String cleanUpCommand = String.join(" ", + "uninstall_default_mime_handler", + desktopFile.installPath().getFileName().toString(), + String.join(" ", getMimeTypeNamesFromFileAssociations())); + + unregisterFileAssociationsCmd = stringifyShellCommands( + unregisterFileAssociationsCmd, cleanUpCommand); + } + + void addIcon(String mimeType, Path iconFile) { + addIcon(mimeType, iconFile, getSquareSizeOfImage(iconFile.toFile())); + } + + void addIcon(String mimeType, Path iconFile, int imgSize) { + imgSize = normalizeIconSize(imgSize); + final String dashMime = mimeType.replace('/', '-'); + registerIconCmds.add(String.join(" ", "xdg-icon-resource", + "install", "--context", "mimetypes", "--size", + Integer.toString(imgSize), iconFile.toString(), dashMime)); + unregisterIconCmds.add(String.join(" ", "xdg-icon-resource", + "uninstall", dashMime, "--size", Integer.toString(imgSize))); + } + + void applyTo(Map data) { + List cmds = new ArrayList<>(); + + cmds.add(registerDesktopFileCmd); + cmds.add(registerFileAssociationsCmd); + cmds.addAll(registerIconCmds); + data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands(cmds)); + + cmds.clear(); + cmds.add(unregisterDesktopFileCmd); + cmds.add(unregisterFileAssociationsCmd); + cmds.addAll(unregisterIconCmds); + data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands(cmds)); + } + + private String registerDesktopFileCmd; + private String unregisterDesktopFileCmd; + + private String registerFileAssociationsCmd; + private String unregisterFileAssociationsCmd; + + private List registerIconCmds; + private List unregisterIconCmds; + } + + /** + * Desktop integration file. xml, icon, etc. + * Resides somewhere in application installation tree. + * Has two paths: + * - path where it should be placed at package build time; + * - path where it should be installed by package manager; + */ + private class DesktopFile { + + DesktopFile(String fileName) { + installPath = thePackage + .installedApplicationLayout() + .destktopIntegrationDirectory().resolve(fileName); + srcPath = thePackage + .sourceApplicationLayout() + .destktopIntegrationDirectory().resolve(fileName); + } + + private final Path installPath; + private final Path srcPath; + + Path installPath() { + return installPath; + } + + Path srcPath() { + return srcPath; + } + } + + private void appendFileAssociation(XMLStreamWriter xml, + FileAssociation assoc) throws XMLStreamException { + + for (var mimeType : assoc.mimeTypes) { + xml.writeStartElement("mime-type"); + xml.writeAttribute("type", mimeType); + + final String description = assoc.description; + if (description != null && !description.isEmpty()) { + xml.writeStartElement("comment"); + xml.writeCharacters(description); + xml.writeEndElement(); + } + + for (String ext : assoc.extensions) { + xml.writeStartElement("glob"); + xml.writeAttribute("pattern", "*." + ext); + xml.writeEndElement(); + } + + xml.writeEndElement(); + } + } + + private void createFileAssociationsMimeInfoFile() throws IOException { + IOUtils.createXml(mimeInfoFile.srcPath(), xml -> { + xml.writeStartElement("mime-info"); + xml.writeDefaultNamespace( + "http://www.freedesktop.org/standards/shared-mime-info"); + + for (var assoc : associations) { + appendFileAssociation(xml, assoc.data); + } + + xml.writeEndElement(); + }); + } + + private void addFileAssociationIconFiles(ShellCommands shellCommands) + throws IOException { + Set processedMimeTypes = new HashSet<>(); + for (var assoc : associations) { + if (assoc.iconSize <= 0) { + // No icon. + continue; + } + + for (var mimeType : assoc.data.mimeTypes) { + if (processedMimeTypes.contains(mimeType)) { + continue; + } + + processedMimeTypes.add(mimeType); + + // Create icon name for mime type from mime type. + DesktopFile faIconFile = new DesktopFile(mimeType.replace( + File.separatorChar, '-') + IOUtils.getSuffix( + assoc.data.iconPath)); + + IOUtils.copyFile(assoc.data.iconPath.toFile(), + faIconFile.srcPath().toFile()); + + shellCommands.addIcon(mimeType, faIconFile.installPath(), + assoc.iconSize); + } + } + } + + private void createDesktopFile(Map data) throws IOException { + List mimeTypes = getMimeTypeNamesFromFileAssociations(); + data.put("DESKTOP_MIMES", "MimeType=" + String.join(";", mimeTypes)); + + // prepare desktop shortcut + desktopFileResource + .setSubstitutionData(data) + .saveToFile(desktopFile.srcPath()); + } + + private List getMimeTypeNamesFromFileAssociations() { + return associations.stream() + .map(fa -> fa.data.mimeTypes) + .flatMap(List::stream) + .collect(Collectors.toUnmodifiableList()); + } + + private static int getSquareSizeOfImage(File f) { + try { + BufferedImage bi = ImageIO.read(f); + return Math.max(bi.getWidth(), bi.getHeight()); + } catch (IOException e) { + Log.verbose(e); + } + return 0; + } + + private static int normalizeIconSize(int iconSize) { + // If register icon with "uncommon" size, it will be ignored. + // So find the best matching "common" size. + List commonIconSizes = List.of(16, 22, 32, 48, 64, 128); + + int idx = Collections.binarySearch(commonIconSizes, iconSize); + if (idx < 0) { + // Given icon size is greater than the largest common icon size. + return commonIconSizes.get(commonIconSizes.size() - 1); + } + + if (idx == 0) { + // Given icon size is less or equal than the smallest common icon size. + return commonIconSizes.get(idx); + } + + int commonIconSize = commonIconSizes.get(idx); + if (iconSize < commonIconSize) { + // It is better to scale down original icon than to scale it up for + // better visual quality. + commonIconSize = commonIconSizes.get(idx - 1); + } + + return commonIconSize; + } + + private static String stringifyShellCommands(String... commands) { + return stringifyShellCommands(Arrays.asList(commands)); + } + + private static String stringifyShellCommands(List commands) { + return String.join(System.lineSeparator(), commands.stream().filter( + s -> s != null && !s.isEmpty()).collect(Collectors.toList())); + } + + private static class LinuxFileAssociation { + LinuxFileAssociation(FileAssociation fa) { + this.data = fa; + if (fa.iconPath != null && Files.isReadable(fa.iconPath)) { + iconSize = getSquareSizeOfImage(fa.iconPath.toFile()); + } else { + iconSize = -1; + } + } + + final FileAssociation data; + final int iconSize; + } + + private final PlatformPackage thePackage; + + private final List associations; + + private final List> launchers; + + private final OverridableResource iconResource; + private final OverridableResource desktopFileResource; + + private final DesktopFile mimeInfoFile; + private final DesktopFile desktopFile; + private final DesktopFile iconFile; + + private final List nestedIntegrations; + + private final Map desktopFileData; + + private static final BundlerParamInfo MENU_GROUP = + new StandardBundlerParam<>( + Arguments.CLIOptions.LINUX_MENU_GROUP.getId(), + String.class, + params -> I18N.getString("param.menu-group.default"), + (s, p) -> s + ); + + private static final StandardBundlerParam SHORTCUT_HINT = + new StandardBundlerParam<>( + Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId(), + Boolean.class, + params -> false, + (s, p) -> (s == null || "null".equalsIgnoreCase(s)) + ? false : Boolean.valueOf(s) + ); +} diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LibProvidersLookup.java b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LibProvidersLookup.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LibProvidersLookup.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Builds list of packages providing dynamic libraries for the given set of files. + */ +final public class LibProvidersLookup { + static boolean supported() { + return (new ToolValidator(TOOL_LDD).validate() == null); + } + + public LibProvidersLookup() { + } + + LibProvidersLookup setPackageLookup(PackageLookup v) { + packageLookup = v; + return this; + } + + List execute(Path root) throws IOException { + // Get the list of files in the root for which to look up for needed shared libraries + List allPackageFiles; + try (Stream stream = Files.walk(root)) { + allPackageFiles = stream.filter(Files::isRegularFile).filter( + LibProvidersLookup::canDependOnLibs).collect( + Collectors.toList()); + } + + Collection neededLibs = getNeededLibsForFiles(allPackageFiles); + + // Get the list of unique package names. + List neededPackages = neededLibs.stream().map(libPath -> { + try { + List packageNames = packageLookup.apply(libPath).filter( + Objects::nonNull).filter(Predicate.not(String::isBlank)).distinct().collect( + Collectors.toList()); + Log.verbose(String.format("%s is provided by %s", libPath, packageNames)); + return packageNames; + } catch (IOException ex) { + // Ignore and keep going + Log.verbose(ex); + List packageNames = Collections.emptyList(); + return packageNames; + } + }).flatMap(List::stream).sorted().distinct().collect(Collectors.toList()); + + return neededPackages; + } + + private static List getNeededLibsForFile(Path path) throws IOException { + List result = new ArrayList<>(); + int ret = Executor.of(TOOL_LDD, path.toString()).setOutputConsumer(lines -> { + lines.map(line -> { + Matcher matcher = LIB_IN_LDD_OUTPUT_REGEX.matcher(line); + if (matcher.find()) { + return matcher.group(1); + } + return null; + }).filter(Objects::nonNull).map(Path::of).forEach(result::add); + }).execute(); + + if (ret != 0) { + // objdump failed. This is OK if the tool was applied to not a binary file + return Collections.emptyList(); + } + + return result; + } + + private static Collection getNeededLibsForFiles(List paths) { + // Depending on tool used, the set can contain full paths (ldd) or + // only file names (objdump). + Set allLibs = paths.stream().map(path -> { + List libs; + try { + libs = getNeededLibsForFile(path); + } catch (IOException ex) { + Log.verbose(ex); + libs = Collections.emptyList(); + } + return libs; + }).flatMap(List::stream).collect(Collectors.toSet()); + + // `allLibs` contains names of all .so needed by files from `paths` list. + // If there are mutual dependencies between binaries from `paths` list, + // then names or full paths to these binaries are in `allLibs` set. + // Remove these items from `allLibs`. + Set excludedNames = paths.stream().map(Path::getFileName).collect( + Collectors.toSet()); + Iterator it = allLibs.iterator(); + while (it.hasNext()) { + Path libName = it.next().getFileName(); + if (excludedNames.contains(libName)) { + it.remove(); + } + } + + return allLibs; + } + + private static boolean canDependOnLibs(Path path) { + return path.toFile().canExecute() || path.toString().endsWith(".so"); + } + + @FunctionalInterface + public interface PackageLookup { + Stream apply(Path path) throws IOException; + } + + private PackageLookup packageLookup; + + private static final String TOOL_LDD = "ldd"; + + // + // Typical ldd output: + // + // ldd: warning: you do not have execution permission for `/tmp/jdk.incubator.jpackage17911687595930080396/images/opt/simplepackagetest/lib/runtime/lib/libawt_headless.so' + // linux-vdso.so.1 => (0x00007ffce6bfd000) + // libawt.so => /tmp/jdk.incubator.jpackage17911687595930080396/images/opt/simplepackagetest/lib/runtime/lib/libawt.so (0x00007f4e00c75000) + // libjvm.so => not found + // libjava.so => /tmp/jdk.incubator.jpackage17911687595930080396/images/opt/simplepackagetest/lib/runtime/lib/libjava.so (0x00007f4e00c41000) + // libm.so.6 => /lib64/libm.so.6 (0x00007f4e00834000) + // libdl.so.2 => /lib64/libdl.so.2 (0x00007f4e00630000) + // libc.so.6 => /lib64/libc.so.6 (0x00007f4e00262000) + // libjvm.so => not found + // libjvm.so => not found + // libverify.so => /tmp/jdk.incubator.jpackage17911687595930080396/images/opt/simplepackagetest/lib/runtime/lib/libverify.so (0x00007f4e00c2e000) + // /lib64/ld-linux-x86-64.so.2 (0x00007f4e00b36000) + // libjvm.so => not found + // + private static final Pattern LIB_IN_LDD_OUTPUT_REGEX = Pattern.compile( + "^\\s*\\S+\\s*=>\\s*(\\S+)\\s+\\(0[xX]\\p{XDigit}+\\)"); +} diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppBundler.java b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppBundler.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.text.MessageFormat; +import java.util.*; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + +public class LinuxAppBundler extends AbstractImageBundler { + + static final BundlerParamInfo ICON_PNG = + new StandardBundlerParam<>( + "icon.png", + File.class, + params -> { + File f = ICON.fetchFrom(params); + if (f != null && !f.getName().toLowerCase().endsWith(".png")) { + Log.error(MessageFormat.format( + I18N.getString("message.icon-not-png"), f)); + return null; + } + return f; + }, + (s, p) -> new File(s)); + + static final BundlerParamInfo LINUX_INSTALL_DIR = + new StandardBundlerParam<>( + "linux-install-dir", + String.class, + params -> { + String dir = INSTALL_DIR.fetchFrom(params); + if (dir != null) { + if (dir.endsWith("/")) { + dir = dir.substring(0, dir.length()-1); + } + return dir; + } + return "/opt"; + }, + (s, p) -> s + ); + + static final BundlerParamInfo LINUX_PACKAGE_DEPENDENCIES = + new StandardBundlerParam<>( + Arguments.CLIOptions.LINUX_PACKAGE_DEPENDENCIES.getId(), + String.class, + params -> { + return ""; + }, + (s, p) -> s + ); + + @Override + public boolean validate(Map params) + throws ConfigException { + try { + Objects.requireNonNull(params); + return doValidate(params); + } catch (RuntimeException re) { + if (re.getCause() instanceof ConfigException) { + throw (ConfigException) re.getCause(); + } else { + throw new ConfigException(re); + } + } + } + + private boolean doValidate(Map params) + throws ConfigException { + + imageBundleValidation(params); + + return true; + } + + File doBundle(Map params, File outputDirectory, + boolean dependentTask) throws PackagerException { + if (StandardBundlerParam.isRuntimeInstaller(params)) { + return PREDEFINED_RUNTIME_IMAGE.fetchFrom(params); + } else { + return doAppBundle(params, outputDirectory, dependentTask); + } + } + + private File doAppBundle(Map params, + File outputDirectory, boolean dependentTask) + throws PackagerException { + try { + File rootDirectory = createRoot(params, outputDirectory, + dependentTask, APP_NAME.fetchFrom(params)); + AbstractAppImageBuilder appBuilder = new LinuxAppImageBuilder( + params, outputDirectory.toPath()); + if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(params) == null ) { + JLinkBundlerHelper.execute(params, appBuilder); + } else { + StandardBundlerParam.copyPredefinedRuntimeImage( + params, appBuilder); + } + return rootDirectory; + } catch (PackagerException pe) { + throw pe; + } catch (Exception ex) { + Log.verbose(ex); + throw new PackagerException(ex); + } + } + + @Override + public String getName() { + return I18N.getString("app.bundler.name"); + } + + @Override + public String getID() { + return "linux.app"; + } + + @Override + public String getBundleType() { + return "IMAGE"; + } + + @Override + public File execute(Map params, + File outputParentDir) throws PackagerException { + return doBundle(params, outputParentDir, false); + } + + @Override + public boolean supported(boolean runtimeInstaller) { + return true; + } + + @Override + public boolean isDefault() { + return false; + } + +} diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppImageBuilder.java b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppImageBuilder.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppImageBuilder.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import static jdk.incubator.jpackage.internal.OverridableResource.createResource; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + +public class LinuxAppImageBuilder extends AbstractAppImageBuilder { + + private static final String LIBRARY_NAME = "libapplauncher.so"; + final static String DEFAULT_ICON = "java32.png"; + + private final ApplicationLayout appLayout; + + public static final BundlerParamInfo ICON_PNG = + new StandardBundlerParam<>( + "icon.png", + File.class, + params -> { + File f = ICON.fetchFrom(params); + if (f != null && !f.getName().toLowerCase().endsWith(".png")) { + Log.error(MessageFormat.format(I18N.getString( + "message.icon-not-png"), f)); + return null; + } + return f; + }, + (s, p) -> new File(s)); + + private static ApplicationLayout createAppLayout(Map params, + Path imageOutDir) { + return ApplicationLayout.linuxAppImage().resolveAt( + imageOutDir.resolve(APP_NAME.fetchFrom(params))); + } + + public LinuxAppImageBuilder(Map params, Path imageOutDir) + throws IOException { + super(params, createAppLayout(params, imageOutDir).runtimeDirectory()); + + appLayout = createAppLayout(params, imageOutDir); + } + + private void writeEntry(InputStream in, Path dstFile) throws IOException { + Files.createDirectories(dstFile.getParent()); + Files.copy(in, dstFile); + } + + public static String getLauncherName(Map params) { + return APP_NAME.fetchFrom(params); + } + + private Path getLauncherCfgPath(Map params) { + return appLayout.appDirectory().resolve( + APP_NAME.fetchFrom(params) + ".cfg"); + } + + @Override + public Path getAppDir() { + return appLayout.appDirectory(); + } + + @Override + public Path getAppModsDir() { + return appLayout.appModsDirectory(); + } + + @Override + protected String getCfgAppDir() { + return Path.of("$ROOTDIR").resolve( + ApplicationLayout.linuxAppImage().appDirectory()).toString() + + File.separator; + } + + @Override + protected String getCfgRuntimeDir() { + return Path.of("$ROOTDIR").resolve( + ApplicationLayout.linuxAppImage().runtimeDirectory()).toString(); + } + + @Override + public void prepareApplicationFiles(Map params) + throws IOException { + Map originalParams = new HashMap<>(params); + + appLayout.roots().stream().forEach(dir -> { + try { + IOUtils.writableOutputDir(dir); + } catch (PackagerException pe) { + throw new RuntimeException(pe); + } + }); + + // create the primary launcher + createLauncherForEntryPoint(params); + + // Copy library to the launcher folder + try (InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) { + writeEntry(is_lib, appLayout.dllDirectory().resolve(LIBRARY_NAME)); + } + + // create the additional launchers, if any + List> entryPoints + = StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params); + for (Map entryPoint : entryPoints) { + createLauncherForEntryPoint( + AddLauncherArguments.merge(originalParams, entryPoint)); + } + + // Copy class path entries to Java folder + copyApplication(params); + + // Copy icon to Resources folder + copyIcon(params); + } + + @Override + public void prepareJreFiles(Map params) + throws IOException {} + + private void createLauncherForEntryPoint( + Map params) throws IOException { + // Copy executable to launchers folder + Path executableFile = appLayout.launchersDirectory().resolve(getLauncherName(params)); + try (InputStream is_launcher = + getResourceAsStream("jpackageapplauncher")) { + writeEntry(is_launcher, executableFile); + } + + executableFile.toFile().setExecutable(true, false); + executableFile.toFile().setWritable(true, true); + + writeCfgFile(params, getLauncherCfgPath(params).toFile()); + } + + private void copyIcon(Map params) + throws IOException { + + Path iconTarget = appLayout.destktopIntegrationDirectory().resolve( + APP_NAME.fetchFrom(params) + IOUtils.getSuffix(Path.of( + DEFAULT_ICON))); + + createResource(DEFAULT_ICON, params) + .setCategory("icon") + .setExternal(ICON_PNG.fetchFrom(params)) + .saveToFile(iconTarget); + } + + private void copyApplication(Map params) + throws IOException { + for (RelativeFileSet appResources : + APP_RESOURCES_LIST.fetchFrom(params)) { + if (appResources == null) { + throw new RuntimeException("Null app resources?"); + } + File srcdir = appResources.getBaseDirectory(); + for (String fname : appResources.getIncludedFiles()) { + copyEntry(appLayout.appDirectory(), srcdir, fname); + } + } + } + +} diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxDebBundler.java b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxDebBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxDebBundler.java @@ -0,0 +1,487 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.*; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.text.MessageFormat; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR; +import static jdk.incubator.jpackage.internal.OverridableResource.createResource; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + + +public class LinuxDebBundler extends LinuxPackageBundler { + + // Debian rules for package naming are used here + // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Source + // + // Package names must consist only of lower case letters (a-z), + // digits (0-9), plus (+) and minus (-) signs, and periods (.). + // They must be at least two characters long and + // must start with an alphanumeric character. + // + private static final Pattern DEB_PACKAGE_NAME_PATTERN = + Pattern.compile("^[a-z][a-z\\d\\+\\-\\.]+"); + + private static final BundlerParamInfo PACKAGE_NAME = + new StandardBundlerParam<> ( + Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(), + String.class, + params -> { + String nm = APP_NAME.fetchFrom(params); + + if (nm == null) return null; + + // make sure to lower case and spaces/underscores become dashes + nm = nm.toLowerCase().replaceAll("[ _]", "-"); + return nm; + }, + (s, p) -> { + if (!DEB_PACKAGE_NAME_PATTERN.matcher(s).matches()) { + throw new IllegalArgumentException(new ConfigException( + MessageFormat.format(I18N.getString( + "error.invalid-value-for-package-name"), s), + I18N.getString( + "error.invalid-value-for-package-name.advice"))); + } + + return s; + }); + + private final static String TOOL_DPKG_DEB = "dpkg-deb"; + private final static String TOOL_DPKG = "dpkg"; + private final static String TOOL_FAKEROOT = "fakeroot"; + + private final static String DEB_ARCH; + static { + String debArch; + try { + debArch = Executor.of(TOOL_DPKG, "--print-architecture").saveOutput( + true).executeExpectSuccess().getOutput().get(0); + } catch (IOException ex) { + debArch = null; + } + DEB_ARCH = debArch; + } + + private static final BundlerParamInfo FULL_PACKAGE_NAME = + new StandardBundlerParam<>( + "linux.deb.fullPackageName", String.class, params -> { + return PACKAGE_NAME.fetchFrom(params) + + "_" + VERSION.fetchFrom(params) + + "-" + RELEASE.fetchFrom(params) + + "_" + DEB_ARCH; + }, (s, p) -> s); + + private static final BundlerParamInfo EMAIL = + new StandardBundlerParam<> ( + Arguments.CLIOptions.LINUX_DEB_MAINTAINER.getId(), + String.class, + params -> "Unknown", + (s, p) -> s); + + private static final BundlerParamInfo MAINTAINER = + new StandardBundlerParam<> ( + BundleParams.PARAM_MAINTAINER, + String.class, + params -> VENDOR.fetchFrom(params) + " <" + + EMAIL.fetchFrom(params) + ">", + (s, p) -> s); + + private static final BundlerParamInfo SECTION = + new StandardBundlerParam<>( + Arguments.CLIOptions.LINUX_CATEGORY.getId(), + String.class, + params -> "misc", + (s, p) -> s); + + private static final BundlerParamInfo LICENSE_TEXT = + new StandardBundlerParam<> ( + "linux.deb.licenseText", + String.class, + params -> { + try { + String licenseFile = LICENSE_FILE.fetchFrom(params); + if (licenseFile != null) { + return Files.readString(Path.of(licenseFile)); + } + } catch (IOException e) { + Log.verbose(e); + } + return "Unknown"; + }, + (s, p) -> s); + + public LinuxDebBundler() { + super(PACKAGE_NAME); + } + + @Override + public void doValidate(Map params) + throws ConfigException { + + // Show warning if license file is missing + if (LICENSE_FILE.fetchFrom(params) == null) { + Log.verbose(I18N.getString("message.debs-like-licenses")); + } + } + + @Override + protected List getToolValidators( + Map params) { + return Stream.of(TOOL_DPKG_DEB, TOOL_DPKG, TOOL_FAKEROOT).map( + ToolValidator::new).collect(Collectors.toList()); + } + + @Override + protected File buildPackageBundle( + Map replacementData, + Map params, File outputParentDir) throws + PackagerException, IOException { + + prepareProjectConfig(replacementData, params); + adjustPermissionsRecursive(createMetaPackage(params).sourceRoot().toFile()); + return buildDeb(params, outputParentDir); + } + + private static final Pattern PACKAGE_NAME_REGEX = Pattern.compile("^(^\\S+):"); + + @Override + protected void initLibProvidersLookup( + Map params, + LibProvidersLookup libProvidersLookup) { + + // + // `dpkg -S` command does glob pattern lookup. If not the absolute path + // to the file is specified it might return mltiple package names. + // Even for full paths multiple package names can be returned as + // it is OK for multiple packages to provide the same file. `/opt` + // directory is such an example. So we have to deal with multiple + // packages per file situation. + // + // E.g.: `dpkg -S libc.so.6` command reports three packages: + // libc6-x32: /libx32/libc.so.6 + // libc6:amd64: /lib/x86_64-linux-gnu/libc.so.6 + // libc6-i386: /lib32/libc.so.6 + // `:amd64` is architecture suffix and can (should) be dropped. + // Still need to decide what package to choose from three. + // libc6-x32 and libc6-i386 both depend on libc6: + // $ dpkg -s libc6-x32 + // Package: libc6-x32 + // Status: install ok installed + // Priority: optional + // Section: libs + // Installed-Size: 10840 + // Maintainer: Ubuntu Developers + // Architecture: amd64 + // Source: glibc + // Version: 2.23-0ubuntu10 + // Depends: libc6 (= 2.23-0ubuntu10) + // + // We can dive into tracking dependencies, but this would be overly + // complicated. + // + // For simplicity lets consider the following rules: + // 1. If there is one item in `dpkg -S` output, accept it. + // 2. If there are multiple items in `dpkg -S` output and there is at + // least one item with the default arch suffix (DEB_ARCH), + // accept only these items. + // 3. If there are multiple items in `dpkg -S` output and there are + // no with the default arch suffix (DEB_ARCH), accept all items. + // So lets use this heuristics: don't accept packages for whom + // `dpkg -p` command fails. + // 4. Arch suffix should be stripped from accepted package names. + // + + libProvidersLookup.setPackageLookup(file -> { + Set archPackages = new HashSet<>(); + Set otherPackages = new HashSet<>(); + + Executor.of(TOOL_DPKG, "-S", file.toString()) + .saveOutput(true).executeExpectSuccess() + .getOutput().forEach(line -> { + Matcher matcher = PACKAGE_NAME_REGEX.matcher(line); + if (matcher.find()) { + String name = matcher.group(1); + if (name.endsWith(":" + DEB_ARCH)) { + // Strip arch suffix + name = name.substring(0, + name.length() - (DEB_ARCH.length() + 1)); + archPackages.add(name); + } else { + otherPackages.add(name); + } + } + }); + + if (!archPackages.isEmpty()) { + return archPackages.stream(); + } + return otherPackages.stream(); + }); + } + + @Override + protected List verifyOutputBundle( + Map params, Path packageBundle) { + List errors = new ArrayList<>(); + + String controlFileName = "control"; + + List properties = List.of( + new PackageProperty("Package", PACKAGE_NAME.fetchFrom(params), + "APPLICATION_PACKAGE", controlFileName), + new PackageProperty("Version", String.format("%s-%s", + VERSION.fetchFrom(params), RELEASE.fetchFrom(params)), + "APPLICATION_VERSION-APPLICATION_RELEASE", + controlFileName), + new PackageProperty("Architecture", DEB_ARCH, "APPLICATION_ARCH", + controlFileName)); + + List cmdline = new ArrayList<>(List.of(TOOL_DPKG_DEB, "-f", + packageBundle.toString())); + properties.forEach(property -> cmdline.add(property.name)); + try { + Map actualValues = Executor.of(cmdline.toArray(String[]::new)) + .saveOutput(true) + .executeExpectSuccess() + .getOutput().stream() + .map(line -> line.split(":\\s+", 2)) + .collect(Collectors.toMap( + components -> components[0], + components -> components[1])); + properties.forEach(property -> errors.add(property.verifyValue( + actualValues.get(property.name)))); + } catch (IOException ex) { + // Ignore error as it is not critical. Just report it. + Log.verbose(ex); + } + + return errors; + } + + /* + * set permissions with a string like "rwxr-xr-x" + * + * This cannot be directly backport to 22u which is built with 1.6 + */ + private void setPermissions(File file, String permissions) { + Set filePermissions = + PosixFilePermissions.fromString(permissions); + try { + if (file.exists()) { + Files.setPosixFilePermissions(file.toPath(), filePermissions); + } + } catch (IOException ex) { + Log.error(ex.getMessage()); + Log.verbose(ex); + } + + } + + public static boolean isDebian() { + // we are just going to run "dpkg -s coreutils" and assume Debian + // or deritive if no error is returned. + try { + Executor.of(TOOL_DPKG, "-s", "coreutils").executeExpectSuccess(); + return true; + } catch (IOException e) { + // just fall thru + } + return false; + } + + private void adjustPermissionsRecursive(File dir) throws IOException { + Files.walkFileTree(dir.toPath(), new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attrs) + throws IOException { + if (file.endsWith(".so") || !Files.isExecutable(file)) { + setPermissions(file.toFile(), "rw-r--r--"); + } else if (Files.isExecutable(file)) { + setPermissions(file.toFile(), "rwxr-xr-x"); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) + throws IOException { + if (e == null) { + setPermissions(dir.toFile(), "rwxr-xr-x"); + return FileVisitResult.CONTINUE; + } else { + // directory iteration failed + throw e; + } + } + }); + } + + private class DebianFile { + + DebianFile(Path dstFilePath, String comment) { + this.dstFilePath = dstFilePath; + this.comment = comment; + } + + DebianFile setExecutable() { + permissions = "rwxr-xr-x"; + return this; + } + + void create(Map data, Map params) + throws IOException { + createResource("template." + dstFilePath.getFileName().toString(), + params) + .setCategory(I18N.getString(comment)) + .setSubstitutionData(data) + .saveToFile(dstFilePath); + if (permissions != null) { + setPermissions(dstFilePath.toFile(), permissions); + } + } + + private final Path dstFilePath; + private final String comment; + private String permissions; + } + + private void prepareProjectConfig(Map data, + Map params) throws IOException { + + Path configDir = createMetaPackage(params).sourceRoot().resolve("DEBIAN"); + List debianFiles = new ArrayList<>(); + debianFiles.add(new DebianFile( + configDir.resolve("control"), + "resource.deb-control-file")); + debianFiles.add(new DebianFile( + configDir.resolve("preinst"), + "resource.deb-preinstall-script").setExecutable()); + debianFiles.add(new DebianFile( + configDir.resolve("prerm"), + "resource.deb-prerm-script").setExecutable()); + debianFiles.add(new DebianFile( + configDir.resolve("postinst"), + "resource.deb-postinstall-script").setExecutable()); + debianFiles.add(new DebianFile( + configDir.resolve("postrm"), + "resource.deb-postrm-script").setExecutable()); + + if (!StandardBundlerParam.isRuntimeInstaller(params)) { + debianFiles.add(new DebianFile( + getConfig_CopyrightFile(params).toPath(), + "resource.copyright-file")); + } + + for (DebianFile debianFile : debianFiles) { + debianFile.create(data, params); + } + } + + @Override + protected Map createReplacementData( + Map params) throws IOException { + Map data = new HashMap<>(); + + data.put("APPLICATION_MAINTAINER", MAINTAINER.fetchFrom(params)); + data.put("APPLICATION_SECTION", SECTION.fetchFrom(params)); + data.put("APPLICATION_COPYRIGHT", COPYRIGHT.fetchFrom(params)); + data.put("APPLICATION_LICENSE_TEXT", LICENSE_TEXT.fetchFrom(params)); + data.put("APPLICATION_ARCH", DEB_ARCH); + data.put("APPLICATION_INSTALLED_SIZE", Long.toString( + createMetaPackage(params).sourceApplicationLayout().sizeInBytes() >> 10)); + + return data; + } + + private File getConfig_CopyrightFile(Map params) { + PlatformPackage thePackage = createMetaPackage(params); + return thePackage.sourceRoot().resolve(Path.of(".", + LINUX_INSTALL_DIR.fetchFrom(params), PACKAGE_NAME.fetchFrom( + params), "share/doc/copyright")).toFile(); + } + + private File buildDeb(Map params, + File outdir) throws IOException { + File outFile = new File(outdir, + FULL_PACKAGE_NAME.fetchFrom(params)+".deb"); + Log.verbose(MessageFormat.format(I18N.getString( + "message.outputting-to-location"), outFile.getAbsolutePath())); + + PlatformPackage thePackage = createMetaPackage(params); + + List cmdline = new ArrayList<>(); + cmdline.addAll(List.of(TOOL_FAKEROOT, TOOL_DPKG_DEB)); + if (Log.isVerbose()) { + cmdline.add("--verbose"); + } + cmdline.addAll(List.of("-b", thePackage.sourceRoot().toString(), + outFile.getAbsolutePath())); + + // run dpkg + Executor.of(cmdline.toArray(String[]::new)).executeExpectSuccess(); + + Log.verbose(MessageFormat.format(I18N.getString( + "message.output-to-location"), outFile.getAbsolutePath())); + + return outFile; + } + + @Override + public String getName() { + return I18N.getString("deb.bundler.name"); + } + + @Override + public String getID() { + return "deb"; + } + + @Override + public boolean supported(boolean runtimeInstaller) { + return Platform.isLinux() && (new ToolValidator(TOOL_DPKG_DEB).validate() == null); + } + + @Override + public boolean isDefault() { + return isDebian(); + } +} diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxPackageBundler.java b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxPackageBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxPackageBundler.java @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.*; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import static jdk.incubator.jpackage.internal.DesktopIntegration.*; +import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR; +import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES; +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + + +abstract class LinuxPackageBundler extends AbstractBundler { + + LinuxPackageBundler(BundlerParamInfo packageName) { + this.packageName = packageName; + } + + @Override + final public boolean validate(Map params) + throws ConfigException { + + // run basic validation to ensure requirements are met + // we are not interested in return code, only possible exception + APP_BUNDLER.fetchFrom(params).validate(params); + + validateInstallDir(LINUX_INSTALL_DIR.fetchFrom(params)); + + validateFileAssociations(FILE_ASSOCIATIONS.fetchFrom(params)); + + // If package name has some restrictions, the string converter will + // throw an exception if invalid + packageName.getStringConverter().apply(packageName.fetchFrom(params), + params); + + for (var validator: getToolValidators(params)) { + ConfigException ex = validator.validate(); + if (ex != null) { + throw ex; + } + } + + withFindNeededPackages = LibProvidersLookup.supported(); + if (!withFindNeededPackages) { + final String advice; + if ("deb".equals(getID())) { + advice = "message.deb-ldd-not-available.advice"; + } else { + advice = "message.rpm-ldd-not-available.advice"; + } + // Let user know package dependencies will not be generated. + Log.error(String.format("%s\n%s", I18N.getString( + "message.ldd-not-available"), I18N.getString(advice))); + } + + // Packaging specific validation + doValidate(params); + + return true; + } + + @Override + final public String getBundleType() { + return "INSTALLER"; + } + + @Override + final public File execute(Map params, + File outputParentDir) throws PackagerException { + IOUtils.writableOutputDir(outputParentDir.toPath()); + + PlatformPackage thePackage = createMetaPackage(params); + + Function initAppImageLayout = imageRoot -> { + ApplicationLayout layout = appImageLayout(params); + layout.pathGroup().setPath(new Object(), + AppImageFile.getPathInAppImage(Path.of(""))); + return layout.resolveAt(imageRoot.toPath()); + }; + + try { + File appImage = StandardBundlerParam.getPredefinedAppImage(params); + + // we either have an application image or need to build one + if (appImage != null) { + initAppImageLayout.apply(appImage).copy( + thePackage.sourceApplicationLayout()); + } else { + appImage = APP_BUNDLER.fetchFrom(params).doBundle(params, + thePackage.sourceRoot().toFile(), true); + ApplicationLayout srcAppLayout = initAppImageLayout.apply( + appImage); + if (appImage.equals(PREDEFINED_RUNTIME_IMAGE.fetchFrom(params))) { + // Application image points to run-time image. + // Copy it. + srcAppLayout.copy(thePackage.sourceApplicationLayout()); + } else { + // Application image is a newly created directory tree. + // Move it. + srcAppLayout.move(thePackage.sourceApplicationLayout()); + if (appImage.exists()) { + // Empty app image directory might remain after all application + // directories have been moved. + appImage.delete(); + } + } + } + + if (!StandardBundlerParam.isRuntimeInstaller(params)) { + desktopIntegration = new DesktopIntegration(thePackage, params); + } else { + desktopIntegration = null; + } + + Map data = createDefaultReplacementData(params); + if (desktopIntegration != null) { + data.putAll(desktopIntegration.create()); + } else { + Stream.of(DESKTOP_COMMANDS_INSTALL, DESKTOP_COMMANDS_UNINSTALL, + UTILITY_SCRIPTS).forEach(v -> data.put(v, "")); + } + + data.putAll(createReplacementData(params)); + + File packageBundle = buildPackageBundle(Collections.unmodifiableMap( + data), params, outputParentDir); + + verifyOutputBundle(params, packageBundle.toPath()).stream() + .filter(Objects::nonNull) + .forEachOrdered(ex -> { + Log.verbose(ex.getLocalizedMessage()); + Log.verbose(ex.getAdvice()); + }); + + return packageBundle; + } catch (IOException ex) { + Log.verbose(ex); + throw new PackagerException(ex); + } + } + + private List getListOfNeededPackages( + Map params) throws IOException { + + PlatformPackage thePackage = createMetaPackage(params); + + final List xdgUtilsPackage; + if (desktopIntegration != null) { + xdgUtilsPackage = desktopIntegration.requiredPackages(); + } else { + xdgUtilsPackage = Collections.emptyList(); + } + + final List neededLibPackages; + if (withFindNeededPackages) { + LibProvidersLookup lookup = new LibProvidersLookup(); + initLibProvidersLookup(params, lookup); + + neededLibPackages = lookup.execute(thePackage.sourceRoot()); + } else { + neededLibPackages = Collections.emptyList(); + } + + // Merge all package lists together. + // Filter out empty names, sort and remove duplicates. + List result = Stream.of(xdgUtilsPackage, neededLibPackages).flatMap( + List::stream).filter(Predicate.not(String::isEmpty)).sorted().distinct().collect( + Collectors.toList()); + + Log.verbose(String.format("Required packages: %s", result)); + + return result; + } + + private Map createDefaultReplacementData( + Map params) throws IOException { + Map data = new HashMap<>(); + + data.put("APPLICATION_PACKAGE", createMetaPackage(params).name()); + data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params)); + data.put("APPLICATION_VERSION", VERSION.fetchFrom(params)); + data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params)); + data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params)); + + String defaultDeps = String.join(", ", getListOfNeededPackages(params)); + String customDeps = LINUX_PACKAGE_DEPENDENCIES.fetchFrom(params).strip(); + if (!customDeps.isEmpty() && !defaultDeps.isEmpty()) { + customDeps = ", " + customDeps; + } + data.put("PACKAGE_DEFAULT_DEPENDENCIES", defaultDeps); + data.put("PACKAGE_CUSTOM_DEPENDENCIES", customDeps); + + return data; + } + + abstract protected List verifyOutputBundle( + Map params, Path packageBundle); + + abstract protected void initLibProvidersLookup( + Map params, + LibProvidersLookup libProvidersLookup); + + abstract protected List getToolValidators( + Map params); + + abstract protected void doValidate(Map params) + throws ConfigException; + + abstract protected Map createReplacementData( + Map params) throws IOException; + + abstract protected File buildPackageBundle( + Map replacementData, + Map params, File outputParentDir) throws + PackagerException, IOException; + + final protected PlatformPackage createMetaPackage( + Map params) { + return new PlatformPackage() { + @Override + public String name() { + return packageName.fetchFrom(params); + } + + @Override + public Path sourceRoot() { + return IMAGES_ROOT.fetchFrom(params).toPath().toAbsolutePath(); + } + + @Override + public ApplicationLayout sourceApplicationLayout() { + return appImageLayout(params).resolveAt( + applicationInstallDir(sourceRoot())); + } + + @Override + public ApplicationLayout installedApplicationLayout() { + return appImageLayout(params).resolveAt( + applicationInstallDir(Path.of("/"))); + } + + private Path applicationInstallDir(Path root) { + Path installDir = Path.of(LINUX_INSTALL_DIR.fetchFrom(params), + name()); + if (installDir.isAbsolute()) { + installDir = Path.of("." + installDir.toString()).normalize(); + } + return root.resolve(installDir); + } + }; + } + + private ApplicationLayout appImageLayout( + Map params) { + if (StandardBundlerParam.isRuntimeInstaller(params)) { + return ApplicationLayout.javaRuntime(); + } + return ApplicationLayout.linuxAppImage(); + } + + private static void validateInstallDir(String installDir) throws + ConfigException { + if (installDir.startsWith("/usr/") || installDir.equals("/usr")) { + throw new ConfigException(MessageFormat.format(I18N.getString( + "error.unsupported-install-dir"), installDir), null); + } + + if (installDir.isEmpty()) { + throw new ConfigException(MessageFormat.format(I18N.getString( + "error.invalid-install-dir"), "/"), null); + } + + boolean valid = false; + try { + final Path installDirPath = Path.of(installDir); + valid = installDirPath.isAbsolute(); + if (valid && !installDirPath.normalize().toString().equals( + installDirPath.toString())) { + // Don't allow '/opt/foo/..' or /opt/. + valid = false; + } + } catch (InvalidPathException ex) { + } + + if (!valid) { + throw new ConfigException(MessageFormat.format(I18N.getString( + "error.invalid-install-dir"), installDir), null); + } + } + + private static void validateFileAssociations( + List> associations) throws + ConfigException { + // only one mime type per association, at least one file extention + int assocIdx = 0; + for (var assoc : associations) { + ++assocIdx; + List mimes = FA_CONTENT_TYPE.fetchFrom(assoc); + if (mimes == null || mimes.isEmpty()) { + String msgKey = "error.no-content-types-for-file-association"; + throw new ConfigException( + MessageFormat.format(I18N.getString(msgKey), assocIdx), + I18N.getString(msgKey + ".advise")); + + } + + if (mimes.size() > 1) { + String msgKey = "error.too-many-content-types-for-file-association"; + throw new ConfigException( + MessageFormat.format(I18N.getString(msgKey), assocIdx), + I18N.getString(msgKey + ".advise")); + } + } + } + + private final BundlerParamInfo packageName; + private boolean withFindNeededPackages; + private DesktopIntegration desktopIntegration; + + private static final BundlerParamInfo APP_BUNDLER = + new StandardBundlerParam<>( + "linux.app.bundler", + LinuxAppBundler.class, + (params) -> new LinuxAppBundler(), + null + ); + +} diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxRpmBundler.java b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxRpmBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxRpmBundler.java @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.*; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; +import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR; +import static jdk.incubator.jpackage.internal.OverridableResource.createResource; + +/** + * There are two command line options to configure license information for RPM + * packaging: --linux-rpm-license-type and --license-file. Value of + * --linux-rpm-license-type command line option configures "License:" section + * of RPM spec. Value of --license-file command line option specifies a license + * file to be added to the package. License file is a sort of documentation file + * but it will be installed even if user selects an option to install the + * package without documentation. --linux-rpm-license-type is the primary option + * to set license information. --license-file makes little sense in case of RPM + * packaging. + */ +public class LinuxRpmBundler extends LinuxPackageBundler { + + // Fedora rules for package naming are used here + // https://fedoraproject.org/wiki/Packaging:NamingGuidelines?rd=Packaging/NamingGuidelines + // + // all Fedora packages must be named using only the following ASCII + // characters. These characters are displayed here: + // + // abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._+ + // + private static final Pattern RPM_PACKAGE_NAME_PATTERN = + Pattern.compile("[a-z\\d\\+\\-\\.\\_]+", Pattern.CASE_INSENSITIVE); + + public static final BundlerParamInfo PACKAGE_NAME = + new StandardBundlerParam<> ( + Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(), + String.class, + params -> { + String nm = APP_NAME.fetchFrom(params); + if (nm == null) return null; + + // make sure to lower case and spaces become dashes + nm = nm.toLowerCase().replaceAll("[ ]", "-"); + + return nm; + }, + (s, p) -> { + if (!RPM_PACKAGE_NAME_PATTERN.matcher(s).matches()) { + String msgKey = "error.invalid-value-for-package-name"; + throw new IllegalArgumentException( + new ConfigException(MessageFormat.format( + I18N.getString(msgKey), s), + I18N.getString(msgKey + ".advice"))); + } + + return s; + } + ); + + public static final BundlerParamInfo LICENSE_TYPE = + new StandardBundlerParam<>( + Arguments.CLIOptions.LINUX_RPM_LICENSE_TYPE.getId(), + String.class, + params -> I18N.getString("param.license-type.default"), + (s, p) -> s + ); + + public static final BundlerParamInfo GROUP = + new StandardBundlerParam<>( + Arguments.CLIOptions.LINUX_CATEGORY.getId(), + String.class, + params -> null, + (s, p) -> s); + + private final static String DEFAULT_SPEC_TEMPLATE = "template.spec"; + + public final static String TOOL_RPM = "rpm"; + public final static String TOOL_RPMBUILD = "rpmbuild"; + public final static DottedVersion TOOL_RPMBUILD_MIN_VERSION = DottedVersion.lazy( + "4.0"); + + public LinuxRpmBundler() { + super(PACKAGE_NAME); + } + + @Override + public void doValidate(Map params) + throws ConfigException { + } + + private static ToolValidator createRpmbuildToolValidator() { + Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)"); + return new ToolValidator(TOOL_RPMBUILD).setMinimalVersion( + TOOL_RPMBUILD_MIN_VERSION).setVersionParser(lines -> { + String versionString = lines.limit(1).collect( + Collectors.toList()).get(0); + Matcher matcher = pattern.matcher(versionString); + if (matcher.find()) { + return matcher.group(1); + } + return null; + }); + } + + @Override + protected List getToolValidators( + Map params) { + return List.of(createRpmbuildToolValidator()); + } + + @Override + protected File buildPackageBundle( + Map replacementData, + Map params, File outputParentDir) throws + PackagerException, IOException { + + Path specFile = specFile(params); + + // prepare spec file + createResource(DEFAULT_SPEC_TEMPLATE, params) + .setCategory(I18N.getString("resource.rpm-spec-file")) + .setSubstitutionData(replacementData) + .saveToFile(specFile); + + return buildRPM(params, outputParentDir.toPath()).toFile(); + } + + @Override + protected Map createReplacementData( + Map params) throws IOException { + Map data = new HashMap<>(); + + data.put("APPLICATION_DIRECTORY", Path.of(LINUX_INSTALL_DIR.fetchFrom( + params), PACKAGE_NAME.fetchFrom(params)).toString()); + data.put("APPLICATION_SUMMARY", APP_NAME.fetchFrom(params)); + data.put("APPLICATION_LICENSE_TYPE", LICENSE_TYPE.fetchFrom(params)); + + String licenseFile = LICENSE_FILE.fetchFrom(params); + if (licenseFile != null) { + licenseFile = Path.of(licenseFile).toAbsolutePath().normalize().toString(); + } + data.put("APPLICATION_LICENSE_FILE", licenseFile); + data.put("APPLICATION_GROUP", GROUP.fetchFrom(params)); + + return data; + } + + @Override + protected void initLibProvidersLookup( + Map params, + LibProvidersLookup libProvidersLookup) { + libProvidersLookup.setPackageLookup(file -> { + return Executor.of(TOOL_RPM, + "-q", "--queryformat", "%{name}\\n", + "-q", "--whatprovides", file.toString()) + .saveOutput(true).executeExpectSuccess().getOutput().stream(); + }); + } + + @Override + protected List verifyOutputBundle( + Map params, Path packageBundle) { + List errors = new ArrayList<>(); + + String specFileName = specFile(params).getFileName().toString(); + + try { + List properties = List.of( + new PackageProperty("Name", PACKAGE_NAME.fetchFrom(params), + "APPLICATION_PACKAGE", specFileName), + new PackageProperty("Version", VERSION.fetchFrom(params), + "APPLICATION_VERSION", specFileName), + new PackageProperty("Release", RELEASE.fetchFrom(params), + "APPLICATION_RELEASE", specFileName), + new PackageProperty("Arch", rpmArch(), null, specFileName)); + + List actualValues = Executor.of(TOOL_RPM, "-qp", "--queryformat", + properties.stream().map(entry -> String.format("%%{%s}", + entry.name)).collect(Collectors.joining("\\n")), + packageBundle.toString()).saveOutput(true).executeExpectSuccess().getOutput(); + + Iterator actualValuesIt = actualValues.iterator(); + properties.forEach(property -> errors.add(property.verifyValue( + actualValuesIt.next()))); + } catch (IOException ex) { + // Ignore error as it is not critical. Just report it. + Log.verbose(ex); + } + + return errors; + } + + /** + * Various ways to get rpm arch. Needed to address JDK-8233143. rpmbuild is + * mandatory for rpm packaging, try it first. rpm is optional and may not be + * available, use as the last resort. + */ + private enum RpmArchReader { + Rpmbuild(TOOL_RPMBUILD, "--eval=%{_target_cpu}"), + Rpm(TOOL_RPM, "--eval=%{_target_cpu}"); + + RpmArchReader(String... cmdline) { + this.cmdline = cmdline; + } + + String getRpmArch() throws IOException { + Executor exec = Executor.of(cmdline).saveOutput(true); + if (this == values()[values().length - 1]) { + exec.executeExpectSuccess(); + } else if (exec.execute() != 0) { + return null; + } + + return exec.getOutput().get(0); + } + + private final String[] cmdline; + } + + private String rpmArch() throws IOException { + if (rpmArch == null) { + for (var rpmArchReader : RpmArchReader.values()) { + rpmArch = rpmArchReader.getRpmArch(); + if (rpmArch != null) { + break; + } + } + } + return rpmArch; + } + + private Path specFile(Map params) { + return TEMP_ROOT.fetchFrom(params).toPath().resolve(Path.of("SPECS", + PACKAGE_NAME.fetchFrom(params) + ".spec")); + } + + private Path buildRPM(Map params, + Path outdir) throws IOException { + + Path rpmFile = outdir.toAbsolutePath().resolve(String.format( + "%s-%s-%s.%s.rpm", PACKAGE_NAME.fetchFrom(params), + VERSION.fetchFrom(params), RELEASE.fetchFrom(params), rpmArch())); + + Log.verbose(MessageFormat.format(I18N.getString( + "message.outputting-bundle-location"), + rpmFile.getParent())); + + PlatformPackage thePackage = createMetaPackage(params); + + //run rpmbuild + Executor.of( + TOOL_RPMBUILD, + "-bb", specFile(params).toAbsolutePath().toString(), + "--define", String.format("%%_sourcedir %s", + thePackage.sourceRoot()), + // save result to output dir + "--define", String.format("%%_rpmdir %s", rpmFile.getParent()), + // do not use other system directories to build as current user + "--define", String.format("%%_topdir %s", + TEMP_ROOT.fetchFrom(params).toPath().toAbsolutePath()), + "--define", String.format("%%_rpmfilename %s", rpmFile.getFileName()) + ).executeExpectSuccess(); + + Log.verbose(MessageFormat.format( + I18N.getString("message.output-bundle-location"), + rpmFile.getParent())); + + return rpmFile; + } + + @Override + public String getName() { + return I18N.getString("rpm.bundler.name"); + } + + @Override + public String getID() { + return "rpm"; + } + + @Override + public boolean supported(boolean runtimeInstaller) { + return Platform.isLinux() && (createRpmbuildToolValidator().validate() == null); + } + + @Override + public boolean isDefault() { + return !LinuxDebBundler.isDebian(); + } + + private String rpmArch; +} diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/PackageProperty.java b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/PackageProperty.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/PackageProperty.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.text.MessageFormat; + +final class PackageProperty { + /** + * Constructor + * + * @param name property name + * @param expectedValue expected property value + * @param substString substitution string to be placed in resource file to + * be replaced with the expected property value by jpackage at package build + * time + * @param customResource name of custom resource from resource directory in + * which this package property can be set + */ + PackageProperty(String name, String expectedValue, String substString, + String customResource) { + this.name = name; + this.expectedValue = expectedValue; + this.substString = substString; + this.customResource = customResource; + } + + ConfigException verifyValue(String actualValue) { + if (expectedValue.equals(actualValue)) { + return null; + } + + final String advice; + if (substString != null) { + advice = MessageFormat.format(I18N.getString( + "error.unexpected-package-property.advice"), substString, + actualValue, name, customResource); + } else { + advice = MessageFormat.format(I18N.getString( + "error.unexpected-default-package-property.advice"), name, + customResource); + } + + return new ConfigException(MessageFormat.format(I18N.getString( + "error.unexpected-package-property"), name, + expectedValue, actualValue, customResource, substString), advice); + } + + final String name; + private final String expectedValue; + private final String substString; + private final String customResource; +} diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources.properties b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources.properties @@ -0,0 +1,69 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# +app.bundler.name=Linux Application Image +deb.bundler.name=DEB Bundle +rpm.bundler.name=RPM Bundle + +param.license-type.default=Unknown +param.menu-group.default=Unknown + +resource.deb-control-file=DEB control file +resource.deb-preinstall-script=DEB preinstall script +resource.deb-prerm-script=DEB prerm script +resource.deb-postinstall-script=DEB postinstall script +resource.deb-postrm-script=DEB postrm script +resource.copyright-file=Copyright file +resource.menu-shortcut-descriptor=Menu shortcut descriptor +resource.menu-icon=menu icon +resource.rpm-spec-file=RPM spec file + +error.tool-not-found.advice=Please install required packages +error.tool-old-version.advice=Please install required packages + +error.invalid-install-dir=Invalid installation directory "{0}" +error.unsupported-install-dir=Installing to system directory "{0}" is currently unsupported + +error.no-content-types-for-file-association=No MIME types were specified for File Association number {0} +error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0}. +error.invalid-value-for-package-name=Invalid value "{0}" for the bundle name. +error.invalid-value-for-package-name.advice=Set the "linux-bundle-name" option to a valid Debian package name. Note that the package names must consist only of lower case letters (a-z), digits (0-9), plus (+) and minus (-) signs, and periods (.). They must be at least two characters long and must start with an alphanumeric character. + +message.icon-not-png=The specified icon "{0}" is not a PNG file and will not be used. The default icon will be used in it's place. +message.test-for-tool=Test for [{0}]. Result: {1} +message.outputting-to-location=Generating DEB for installer to: {0}. +message.output-to-location=Package (.deb) saved to: {0}. +message.debs-like-licenses=Debian packages should specify a license. The absence of a license will cause some linux distributions to complain about the quality of the application. +message.outputting-bundle-location=Generating RPM for installer to: {0}. +message.output-bundle-location=Package (.rpm) saved to: {0}. +message.creating-association-with-null-extension=Creating association with null extension. + +message.ldd-not-available=ldd command not found. Package dependencies will not be generated. +message.deb-ldd-not-available.advice=Install "libc-bin" DEB package to get ldd. +message.rpm-ldd-not-available.advice=Install "glibc-common" RPM package to get ldd. + +error.unexpected-package-property=Expected value of "{0}" property is [{1}]. Actual value in output package is [{2}]. Looks like custom "{3}" file from resource directory contained hard coded value of "{0}" property +error.unexpected-package-property.advice=Use [{0}] pattern string instead of hard coded value [{1}] of {2} property in custom "{3}" file +error.unexpected-default-package-property.advice=Don't explicitly set value of {0} property in custom "{1}" file diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources_ja.properties b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources_ja.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources_ja.properties @@ -0,0 +1,69 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# +app.bundler.name=Linux Application Image +deb.bundler.name=DEB Bundle +rpm.bundler.name=RPM Bundle + +param.license-type.default=Unknown +param.menu-group.default=Unknown + +resource.deb-control-file=DEB control file +resource.deb-preinstall-script=DEB preinstall script +resource.deb-prerm-script=DEB prerm script +resource.deb-postinstall-script=DEB postinstall script +resource.deb-postrm-script=DEB postrm script +resource.copyright-file=Copyright file +resource.menu-shortcut-descriptor=Menu shortcut descriptor +resource.menu-icon=menu icon +resource.rpm-spec-file=RPM spec file + +error.tool-not-found.advice=Please install required packages +error.tool-old-version.advice=Please install required packages + +error.invalid-install-dir=Invalid installation directory "{0}" +error.unsupported-install-dir=Installing to system directory "{0}" is currently unsupported + +error.no-content-types-for-file-association=No MIME types were specified for File Association number {0} +error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0}. +error.invalid-value-for-package-name=Invalid value "{0}" for the bundle name. +error.invalid-value-for-package-name.advice=Set the "linux-bundle-name" option to a valid Debian package name. Note that the package names must consist only of lower case letters (a-z), digits (0-9), plus (+) and minus (-) signs, and periods (.). They must be at least two characters long and must start with an alphanumeric character. + +message.icon-not-png=The specified icon "{0}" is not a PNG file and will not be used. The default icon will be used in it's place. +message.test-for-tool=Test for [{0}]. Result: {1} +message.outputting-to-location=Generating DEB for installer to: {0}. +message.output-to-location=Package (.deb) saved to: {0}. +message.debs-like-licenses=Debian packages should specify a license. The absence of a license will cause some linux distributions to complain about the quality of the application. +message.outputting-bundle-location=Generating RPM for installer to: {0}. +message.output-bundle-location=Package (.rpm) saved to: {0}. +message.creating-association-with-null-extension=Creating association with null extension. + +message.ldd-not-available=ldd command not found. Package dependencies will not be generated. +message.deb-ldd-not-available.advice=Install "libc-bin" DEB package to get ldd. +message.rpm-ldd-not-available.advice=Install "glibc-common" RPM package to get ldd. + +error.unexpected-package-property=Expected value of "{0}" property is [{1}]. Actual value in output package is [{2}]. Looks like custom "{3}" file from resource directory contained hard coded value of "{0}" property +error.unexpected-package-property.advice=Use [{0}] pattern string instead of hard coded value [{1}] of {2} property in custom "{3}" file +error.unexpected-default-package-property.advice=Don't explicitly set value of {0} property in custom "{1}" file diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources_zh_CN.properties b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources_zh_CN.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources_zh_CN.properties @@ -0,0 +1,69 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# +app.bundler.name=Linux Application Image +deb.bundler.name=DEB Bundle +rpm.bundler.name=RPM Bundle + +param.license-type.default=Unknown +param.menu-group.default=Unknown + +resource.deb-control-file=DEB control file +resource.deb-preinstall-script=DEB preinstall script +resource.deb-prerm-script=DEB prerm script +resource.deb-postinstall-script=DEB postinstall script +resource.deb-postrm-script=DEB postrm script +resource.copyright-file=Copyright file +resource.menu-shortcut-descriptor=Menu shortcut descriptor +resource.menu-icon=menu icon +resource.rpm-spec-file=RPM spec file + +error.tool-not-found.advice=Please install required packages +error.tool-old-version.advice=Please install required packages + +error.invalid-install-dir=Invalid installation directory "{0}" +error.unsupported-install-dir=Installing to system directory "{0}" is currently unsupported + +error.no-content-types-for-file-association=No MIME types were specified for File Association number {0} +error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0}. +error.invalid-value-for-package-name=Invalid value "{0}" for the bundle name. +error.invalid-value-for-package-name.advice=Set the "linux-bundle-name" option to a valid Debian package name. Note that the package names must consist only of lower case letters (a-z), digits (0-9), plus (+) and minus (-) signs, and periods (.). They must be at least two characters long and must start with an alphanumeric character. + +message.icon-not-png=The specified icon "{0}" is not a PNG file and will not be used. The default icon will be used in it's place. +message.test-for-tool=Test for [{0}]. Result: {1} +message.outputting-to-location=Generating DEB for installer to: {0}. +message.output-to-location=Package (.deb) saved to: {0}. +message.debs-like-licenses=Debian packages should specify a license. The absence of a license will cause some linux distributions to complain about the quality of the application. +message.outputting-bundle-location=Generating RPM for installer to: {0}. +message.output-bundle-location=Package (.rpm) saved to: {0}. +message.creating-association-with-null-extension=Creating association with null extension. + +message.ldd-not-available=ldd command not found. Package dependencies will not be generated. +message.deb-ldd-not-available.advice=Install "libc-bin" DEB package to get ldd. +message.rpm-ldd-not-available.advice=Install "glibc-common" RPM package to get ldd. + +error.unexpected-package-property=Expected value of "{0}" property is [{1}]. Actual value in output package is [{2}]. Looks like custom "{3}" file from resource directory contained hard coded value of "{0}" property +error.unexpected-package-property.advice=Use [{0}] pattern string instead of hard coded value [{1}] of {2} property in custom "{3}" file +error.unexpected-default-package-property.advice=Don't explicitly set value of {0} property in custom "{1}" file diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/java32.png b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/java32.png new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0c41d652a3c6e608a102a8bed7f88fd0f1cc546d GIT binary patch literal 4955 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 Z1^}yv>iSU@Nz4EM002ovPDHLkV1izNR#pH2 diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.control b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.control new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.control @@ -0,0 +1,10 @@ +Package: APPLICATION_PACKAGE +Version: APPLICATION_VERSION-APPLICATION_RELEASE +Section: APPLICATION_SECTION +Maintainer: APPLICATION_MAINTAINER +Priority: optional +Architecture: APPLICATION_ARCH +Provides: APPLICATION_PACKAGE +Description: APPLICATION_DESCRIPTION +Depends: PACKAGE_DEFAULT_DEPENDENCIES PACKAGE_CUSTOM_DEPENDENCIES +Installed-Size: APPLICATION_INSTALLED_SIZE diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.copyright b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.copyright new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.copyright @@ -0,0 +1,5 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: * +Copyright: APPLICATION_COPYRIGHT +License: APPLICATION_LICENSE_TEXT diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.desktop b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.desktop new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=APPLICATION_NAME +Comment=APPLICATION_DESCRIPTION +Exec=APPLICATION_LAUNCHER +Icon=APPLICATION_ICON +Terminal=false +Type=Application +Categories=DEPLOY_BUNDLE_CATEGORY +DESKTOP_MIMES diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.postinst b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.postinst new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.postinst @@ -0,0 +1,34 @@ +#!/bin/sh +# postinst script for APPLICATION_PACKAGE +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see https://www.debian.org/doc/debian-policy/ or +# the debian-policy package + +case "$1" in + configure) +DESKTOP_COMMANDS_INSTALL + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.postrm b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.postrm new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.postrm @@ -0,0 +1,31 @@ +#!/bin/sh +# postrm script for APPLICATION_PACKAGE +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `remove' +# * `purge' +# * `upgrade' +# * `failed-upgrade' +# * `abort-install' +# * `abort-install' +# * `abort-upgrade' +# * `disappear' +# +# for details, see https://www.debian.org/doc/debian-policy/ or +# the debian-policy package + +case "$1" in + purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + ;; + + *) + echo "postrm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.preinst b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.preinst new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.preinst @@ -0,0 +1,30 @@ +#!/bin/sh +# preinst script for APPLICATION_PACKAGE +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `install' +# * `install' +# * `upgrade' +# * `abort-upgrade' +# for details, see https://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + install|upgrade) + ;; + + abort-upgrade) + ;; + + *) + echo "preinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.prerm b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.prerm new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.prerm @@ -0,0 +1,37 @@ +#!/bin/sh +# prerm script for APPLICATION_PACKAGE +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `remove' +# * `upgrade' +# * `failed-upgrade' +# * `remove' `in-favour' +# * `deconfigure' `in-favour' +# `removing' +# +# for details, see https://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +UTILITY_SCRIPTS + +case "$1" in + remove|upgrade|deconfigure) +DESKTOP_COMMANDS_UNINSTALL + ;; + + failed-upgrade) + ;; + + *) + echo "prerm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 + diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.spec b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.spec new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.spec @@ -0,0 +1,58 @@ +Summary: APPLICATION_SUMMARY +Name: APPLICATION_PACKAGE +Version: APPLICATION_VERSION +Release: APPLICATION_RELEASE +License: APPLICATION_LICENSE_TYPE +Vendor: APPLICATION_VENDOR +Prefix: %{dirname:APPLICATION_DIRECTORY} +Provides: APPLICATION_PACKAGE +%if "xAPPLICATION_GROUP" != x +Group: APPLICATION_GROUP +%endif + +Autoprov: 0 +Autoreq: 0 +%if "xPACKAGE_DEFAULT_DEPENDENCIES" != x || "xPACKAGE_CUSTOM_DEPENDENCIES" != x +Requires: PACKAGE_DEFAULT_DEPENDENCIES PACKAGE_CUSTOM_DEPENDENCIES +%endif + +#comment line below to enable effective jar compression +#it could easily get your package size from 40 to 15Mb but +#build time will substantially increase and it may require unpack200/system java to install +%define __jar_repack %{nil} + +%description +APPLICATION_DESCRIPTION + +%prep + +%build + +%install +rm -rf %{buildroot} +install -d -m 755 %{buildroot}APPLICATION_DIRECTORY +cp -r %{_sourcedir}APPLICATION_DIRECTORY/* %{buildroot}APPLICATION_DIRECTORY +%if "xAPPLICATION_LICENSE_FILE" != x + %define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:APPLICATION_LICENSE_FILE} + install -d -m 755 %{buildroot}%{dirname:%{license_install_file}} + install -m 644 APPLICATION_LICENSE_FILE %{buildroot}%{license_install_file} +%endif + +%files +%if "xAPPLICATION_LICENSE_FILE" != x + %license %{license_install_file} + %{dirname:%{license_install_file}} +%endif +# If installation directory for the application is /a/b/c, we want only root +# component of the path (/a) in the spec file to make sure all subdirectories +# are owned by the package. +%(echo APPLICATION_DIRECTORY | sed -e "s|\(^/[^/]\{1,\}\).*$|\1|") + +%post +DESKTOP_COMMANDS_INSTALL + +%preun +UTILITY_SCRIPTS +DESKTOP_COMMANDS_UNINSTALL + +%clean diff --git a/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/utils.sh b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/utils.sh new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/utils.sh @@ -0,0 +1,104 @@ +# +# Remove $1 desktop file from the list of default handlers for $2 mime type +# in $3 file dumping output to stdout. +# +_filter_out_default_mime_handler () +{ + local defaults_list="$3" + + local desktop_file="$1" + local mime_type="$2" + + awk -f- "$defaults_list" < "$tmpfile1" + + local v + local update= + for mime in "$@"; do + _filter_out_default_mime_handler "$desktop_file" "$mime" "$tmpfile1" > "$tmpfile2" + v="$tmpfile2" + tmpfile2="$tmpfile1" + tmpfile1="$v" + + if ! diff -q "$tmpfile1" "$tmpfile2" > /dev/null; then + update=yes + trace Remove $desktop_file default handler for $mime mime type from $defaults_list file + fi + done + + if [ -n "$update" ]; then + cat "$tmpfile1" > "$defaults_list" + trace "$defaults_list" file updated + fi + + rm -f "$tmpfile1" "$tmpfile2" +} + + +# +# Remove $1 desktop file from the list of default handlers for $@ mime types +# in all known system defaults lists. +# +uninstall_default_mime_handler () +{ + for f in /usr/share/applications/defaults.list /usr/local/share/applications/defaults.list; do + _uninstall_default_mime_handler "$f" "$@" + done +} + + +trace () +{ + echo "$@" +} diff --git a/src/jdk.incubator.jpackage/linux/classes/module-info.java.extra b/src/jdk.incubator.jpackage/linux/classes/module-info.java.extra new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/classes/module-info.java.extra @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +provides jdk.incubator.jpackage.internal.Bundler with + jdk.incubator.jpackage.internal.LinuxAppBundler, + jdk.incubator.jpackage.internal.LinuxDebBundler, + jdk.incubator.jpackage.internal.LinuxRpmBundler; + diff --git a/src/jdk.incubator.jpackage/linux/native/jpackageapplauncher/launcher.cpp b/src/jdk.incubator.jpackage/linux/native/jpackageapplauncher/launcher.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/native/jpackageapplauncher/launcher.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include +#include +#include +#include +#include + + +typedef bool (*start_launcher)(int argc, char* argv[]); +typedef void (*stop_launcher)(); + +#define MAX_PATH 1024 + +std::string GetProgramPath() { + ssize_t len = 0; + std::string result; + char buffer[MAX_PATH] = {0}; + + if ((len = readlink("/proc/self/exe", buffer, MAX_PATH - 1)) != -1) { + buffer[len] = '\0'; + result = buffer; + } + + return result; +} + +int main(int argc, char *argv[]) { + int result = 1; + setlocale(LC_ALL, "en_US.utf8"); + void* library = NULL; + + { + std::string programPath = GetProgramPath(); + std::string libraryName = dirname((char*)programPath.c_str()); + libraryName += "/../lib/libapplauncher.so"; + library = dlopen(libraryName.c_str(), RTLD_LAZY); + + if (library == NULL) { + fprintf(stderr, "dlopen failed: %s\n", dlerror()); + fprintf(stderr, "%s not found.\n", libraryName.c_str()); + } + } + + if (library != NULL) { + start_launcher start = (start_launcher)dlsym(library, "start_launcher"); + stop_launcher stop = (stop_launcher)dlsym(library, "stop_launcher"); + + if (start != NULL && stop != NULL) { + if (start(argc, argv) == true) { + result = 0; + stop(); + } + } else { + fprintf(stderr, "cannot find start_launcher and stop_launcher in libapplauncher.so"); + } + + dlclose(library); + } + + + return result; +} diff --git a/src/jdk.incubator.jpackage/linux/native/libapplauncher/LinuxPlatform.cpp b/src/jdk.incubator.jpackage/linux/native/libapplauncher/LinuxPlatform.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/native/libapplauncher/LinuxPlatform.cpp @@ -0,0 +1,1080 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Platform.h" + +#include "JavaVirtualMachine.h" +#include "LinuxPlatform.h" +#include "PlatformString.h" +#include "IniFile.h" +#include "Helpers.h" +#include "FilePath.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LINUX_JPACKAGE_TMP_DIR "/.java/jpackage/tmp" + +TString GetEnv(const TString &name) { + TString result; + + char *value = ::getenv((TCHAR*) name.c_str()); + + if (value != NULL) { + result = value; + } + + return result; +} + +LinuxPlatform::LinuxPlatform(void) : Platform(), +PosixPlatform() { + FMainThread = pthread_self(); +} + +LinuxPlatform::~LinuxPlatform(void) { +} + +TString LinuxPlatform::GetPackageAppDirectory() { + return FilePath::IncludeTrailingSeparator( + GetPackageRootDirectory()) + _T("lib/app"); +} + +TString LinuxPlatform::GetAppName() { + TString result = GetModuleFileName(); + result = FilePath::ExtractFileName(result); + return result; +} + +TString LinuxPlatform::GetPackageLauncherDirectory() { + return FilePath::IncludeTrailingSeparator( + GetPackageRootDirectory()) + _T("bin"); +} + +TString LinuxPlatform::GetPackageRuntimeBinDirectory() { + return FilePath::IncludeTrailingSeparator(GetPackageRootDirectory()) + + _T("runtime/bin"); +} + +void LinuxPlatform::ShowMessage(TString title, TString description) { + printf("%s %s\n", PlatformString(title).toPlatformString(), + PlatformString(description).toPlatformString()); + fflush(stdout); +} + +void LinuxPlatform::ShowMessage(TString description) { + TString appname = GetModuleFileName(); + appname = FilePath::ExtractFileName(appname); + ShowMessage(PlatformString(appname).toPlatformString(), + PlatformString(description).toPlatformString()); +} + +TCHAR* LinuxPlatform::ConvertStringToFileSystemString(TCHAR* Source, + bool &release) { + // Not Implemented. + return NULL; +} + +TCHAR* LinuxPlatform::ConvertFileSystemStringToString(TCHAR* Source, + bool &release) { + // Not Implemented. + return NULL; +} + +TString LinuxPlatform::GetModuleFileName() { + ssize_t len = 0; + TString result; + DynamicBuffer buffer(MAX_PATH); + if (buffer.GetData() == NULL) { + return result; + } + + if ((len = readlink("/proc/self/exe", buffer.GetData(), + MAX_PATH - 1)) != -1) { + buffer[len] = '\0'; + result = buffer.GetData(); + } + + return result; +} + +TString LinuxPlatform::GetPackageRootDirectory() { + TString result; + TString filename = GetModuleFileName(); + TString binPath = FilePath::ExtractFilePath(filename); + + size_t slash = binPath.find_last_of(TRAILING_PATHSEPARATOR); + if (slash != TString::npos) { + result = binPath.substr(0, slash); + } + + return result; +} + +TString LinuxPlatform::GetAppDataDirectory() { + TString result; + TString home = GetEnv(_T("HOME")); + + if (home.empty() == false) { + result += FilePath::IncludeTrailingSeparator(home) + _T(".local"); + } + + return result; +} + +ISectionalPropertyContainer* LinuxPlatform::GetConfigFile(TString FileName) { + IniFile *result = new IniFile(); + if (result == NULL) { + return NULL; + } + + result->LoadFromFile(FileName); + + return result; +} + +TString LinuxPlatform::GetBundledJavaLibraryFileName(TString RuntimePath) { + TString result = FilePath::IncludeTrailingSeparator(RuntimePath) + + "lib/libjli.so"; + + if (FilePath::FileExists(result) == false) { + result = FilePath::IncludeTrailingSeparator(RuntimePath) + + "lib/jli/libjli.so"; + if (FilePath::FileExists(result) == false) { + printf("Cannot find libjli.so!"); + } + } + + return result; +} + +bool LinuxPlatform::IsMainThread() { + bool result = (FMainThread == pthread_self()); + return result; +} + +TString LinuxPlatform::getTmpDirString() { + return TString(LINUX_JPACKAGE_TMP_DIR); +} + +TPlatformNumber LinuxPlatform::GetMemorySize() { + long pages = sysconf(_SC_PHYS_PAGES); + long page_size = sysconf(_SC_PAGE_SIZE); + TPlatformNumber result = pages * page_size; + result = result / 1048576; // Convert from bytes to megabytes. + return result; +} + +void PosixProcess::Cleanup() { + if (FOutputHandle != 0) { + close(FOutputHandle); + FOutputHandle = 0; + } + + if (FInputHandle != 0) { + close(FInputHandle); + FInputHandle = 0; + } +} + +#define PIPE_READ 0 +#define PIPE_WRITE 1 + +bool PosixProcess::Execute(const TString Application, + const std::vector Arguments, bool AWait) { + bool result = false; + + if (FRunning == false) { + FRunning = true; + + int handles[2]; + + if (pipe(handles) == -1) { + return false; + } + + struct sigaction sa; + sa.sa_handler = SIG_IGN; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + FChildPID = fork(); + + // PID returned by vfork is 0 for the child process and the + // PID of the child process for the parent. + if (FChildPID == -1) { + // Error + TString message = PlatformString::Format( + _T("Error: Unable to create process %s"), + Application.data()); + throw Exception(message); + } else if (FChildPID == 0) { + Cleanup(); + TString command = Application; + + for (std::vector::const_iterator iterator = + Arguments.begin(); iterator != Arguments.end(); + iterator++) { + command += TString(_T(" ")) + *iterator; + } +#ifdef DEBUG + printf("%s\n", command.data()); +#endif // DEBUG + + dup2(handles[PIPE_READ], STDIN_FILENO); + dup2(handles[PIPE_WRITE], STDOUT_FILENO); + + close(handles[PIPE_READ]); + close(handles[PIPE_WRITE]); + + execl("/bin/sh", "sh", "-c", command.data(), (char *) 0); + + _exit(127); + } else { + FOutputHandle = handles[PIPE_READ]; + FInputHandle = handles[PIPE_WRITE]; + + if (AWait == true) { + ReadOutput(); + Wait(); + Cleanup(); + FRunning = false; + result = true; + } else { + result = true; + } + } + } + + return result; +} + + +//---------------------------------------------------------------------------- + +#ifndef __UNIX_JPACKAGE_PLATFORM__ +#define __UNIX_JPACKAGE_PLATFORM__ + +/** Provide an abstraction for difference in the platform APIs, + e.g. string manipulation functions, etc. */ +#include +#include +#include +#include + +#define TCHAR char + +#define _T(x) x + +#define JPACKAGE_MULTIBYTE_SNPRINTF snprintf + +#define JPACKAGE_SNPRINTF(buffer, sizeOfBuffer, count, format, ...) \ + snprintf((buffer), (count), (format), __VA_ARGS__) + +#define JPACKAGE_PRINTF(format, ...) \ + printf((format), ##__VA_ARGS__) + +#define JPACKAGE_FPRINTF(dest, format, ...) \ + fprintf((dest), (format), __VA_ARGS__) + +#define JPACKAGE_SSCANF(buf, format, ...) \ + sscanf((buf), (format), __VA_ARGS__) + +#define JPACKAGE_STRDUP(strSource) \ + strdup((strSource)) + +//return "error code" (like on Windows) + +static int JPACKAGE_STRNCPY(char *strDest, size_t numberOfElements, + const char *strSource, size_t count) { + char *s = strncpy(strDest, strSource, count); + // Duplicate behavior of the Windows' _tcsncpy_s() by adding a NULL + // terminator at the end of the string. + if (count < numberOfElements) { + s[count] = '\0'; + } else { + s[numberOfElements - 1] = '\0'; + } + return (s == strDest) ? 0 : 1; +} + +#define JPACKAGE_STRICMP(x, y) \ + strcasecmp((x), (y)) + +#define JPACKAGE_STRNICMP(x, y, cnt) \ + strncasecmp((x), (y), (cnt)) + +#define JPACKAGE_STRNCMP(x, y, cnt) \ + strncmp((x), (y), (cnt)) + +#define JPACKAGE_STRLEN(x) \ + strlen((x)) + +#define JPACKAGE_STRSTR(x, y) \ + strstr((x), (y)) + +#define JPACKAGE_STRCHR(x, y) \ + strchr((x), (y)) + +#define JPACKAGE_STRRCHR(x, y) \ + strrchr((x), (y)) + +#define JPACKAGE_STRPBRK(x, y) \ + strpbrk((x), (y)) + +#define JPACKAGE_GETENV(x) \ + getenv((x)) + +#define JPACKAGE_PUTENV(x) \ + putenv((x)) + +#define JPACKAGE_STRCMP(x, y) \ + strcmp((x), (y)) + +#define JPACKAGE_STRCPY(x, y) \ + strcpy((x), (y)) + +#define JPACKAGE_STRCAT(x, y) \ + strcat((x), (y)) + +#define JPACKAGE_ATOI(x) \ + atoi((x)) + +#define JPACKAGE_FOPEN(x, y) \ + fopen((x), (y)) + +#define JPACKAGE_FGETS(x, y, z) \ + fgets((x), (y), (z)) + +#define JPACKAGE_REMOVE(x) \ + remove((x)) + +#define JPACKAGE_SPAWNV(mode, cmd, args) \ + spawnv((mode), (cmd), (args)) + +#define JPACKAGE_ISDIGIT(ch) isdigit(ch) + +// for non-unicode, just return the input string for +// the following 2 conversions +#define JPACKAGE_NEW_MULTIBYTE(message) message + +#define JPACKAGE_NEW_FROM_MULTIBYTE(message) message + +// for non-unicode, no-op for the relase operation +// since there is no memory allocated for the +// string conversions +#define JPACKAGE_RELEASE_MULTIBYTE(tmpMBCS) + +#define JPACKAGE_RELEASE_FROM_MULTIBYTE(tmpMBCS) + +// The size will be used for converting from 1 byte to 1 byte encoding. +// Ensure have space for zero-terminator. +#define JPACKAGE_GET_SIZE_FOR_ENCODING(message, theLength) (theLength + 1) + +#endif +#define xmlTagType 0 +#define xmlPCDataType 1 + +typedef struct _xmlNode XMLNode; +typedef struct _xmlAttribute XMLAttribute; + +struct _xmlNode { + int _type; // Type of node: tag, pcdata, cdate + TCHAR* _name; // Contents of node + XMLNode* _next; // Next node at same level + XMLNode* _sub; // First sub-node + XMLAttribute* _attributes; // List of attributes +}; + +struct _xmlAttribute { + TCHAR* _name; // Name of attribute + TCHAR* _value; // Value of attribute + XMLAttribute* _next; // Next attribute for this tag +}; + +// Public interface +static void RemoveNonAsciiUTF8FromBuffer(char *buf); +XMLNode* ParseXMLDocument(TCHAR* buf); +void FreeXMLDocument(XMLNode* root); + +// Utility methods for parsing document +XMLNode* FindXMLChild(XMLNode* root, const TCHAR* name); +TCHAR* FindXMLAttribute(XMLAttribute* attr, const TCHAR* name); + +// Debugging +void PrintXMLDocument(XMLNode* node, int indt); + +#include +#include +#include +#include +#include + +#define JWS_assert(s, msg) \ + if (!(s)) { Abort(msg); } + + +// Internal declarations +static XMLNode* ParseXMLElement(void); +static XMLAttribute* ParseXMLAttribute(void); +static TCHAR* SkipWhiteSpace(TCHAR *p); +static TCHAR* SkipXMLName(TCHAR *p); +static TCHAR* SkipXMLComment(TCHAR *p); +static TCHAR* SkipXMLDocType(TCHAR *p); +static TCHAR* SkipXMLProlog(TCHAR *p); +static TCHAR* SkipPCData(TCHAR *p); +static int IsPCData(TCHAR *p); +static void ConvertBuiltInEntities(TCHAR* p); +static void SetToken(int type, TCHAR* start, TCHAR* end); +static void GetNextToken(void); +static XMLNode* CreateXMLNode(int type, TCHAR* name); +static XMLAttribute* CreateXMLAttribute(TCHAR *name, TCHAR* value); +static XMLNode* ParseXMLElement(void); +static XMLAttribute* ParseXMLAttribute(void); +static void FreeXMLAttribute(XMLAttribute* attr); +static void PrintXMLAttributes(XMLAttribute* attr); +static void indent(int indt); + +static jmp_buf jmpbuf; +static XMLNode* root_node = NULL; + +/** definition of error codes for setjmp/longjmp, + * that can be handled in ParseXMLDocument() + */ +#define JMP_NO_ERROR 0 +#define JMP_OUT_OF_RANGE 1 + +#define NEXT_CHAR(p) { \ + if (*p != 0) { \ + p++; \ + } else { \ + longjmp(jmpbuf, JMP_OUT_OF_RANGE); \ + } \ +} +#define NEXT_CHAR_OR_BREAK(p) { \ + if (*p != 0) { \ + p++; \ + } else { \ + break; \ + } \ +} +#define NEXT_CHAR_OR_RETURN(p) { \ + if (*p != 0) { \ + p++; \ + } else { \ + return; \ + } \ +} +#define SKIP_CHARS(p,n) { \ + int i; \ + for (i = 0; i < (n); i++) { \ + if (*p != 0) { \ + p++; \ + } else { \ + longjmp(jmpbuf, JMP_OUT_OF_RANGE); \ + } \ + } \ +} +#define SKIP_CHARS_OR_BREAK(p,n) { \ + int i; \ + for (i = 0; i < (n); i++) { \ + if (*p != 0) { \ + p++; \ + } else { \ + break; \ + } \ + } \ + if (i < (n)) { \ + break; \ + } \ +} + +/** Iterates through the null-terminated buffer (i.e., C string) and + * replaces all UTF-8 encoded character >255 with 255 + * + * UTF-8 encoding: + * + * Range A: 0x0000 - 0x007F + * 0 | bits 0 - 7 + * Range B : 0x0080 - 0x07FF : + * 110 | bits 6 - 10 + * 10 | bits 0 - 5 + * Range C : 0x0800 - 0xFFFF : + * 1110 | bits 12-15 + * 10 | bits 6-11 + * 10 | bits 0-5 + */ +static void RemoveNonAsciiUTF8FromBuffer(char *buf) { + char* p; + char* q; + char c; + p = q = buf; + // We are not using NEXT_CHAR() to check if *q is NULL, as q is output + // location and offset for q is smaller than for p. + while (*p != '\0') { + c = *p; + if ((c & 0x80) == 0) { + /* Range A */ + *q++ = *p; + NEXT_CHAR(p); + } else if ((c & 0xE0) == 0xC0) { + /* Range B */ + *q++ = (char) 0xFF; + NEXT_CHAR(p); + NEXT_CHAR_OR_BREAK(p); + } else { + /* Range C */ + *q++ = (char) 0xFF; + NEXT_CHAR(p); + SKIP_CHARS_OR_BREAK(p, 2); + } + } + /* Null terminate string */ + *q = '\0'; +} + +static TCHAR* SkipWhiteSpace(TCHAR *p) { + if (p != NULL) { + while (iswspace(*p)) + NEXT_CHAR_OR_BREAK(p); + } + return p; +} + +static TCHAR* SkipXMLName(TCHAR *p) { + TCHAR c = *p; + /* Check if start of token */ + if (('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z') || + c == '_' || c == ':') { + + while (('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z') || + ('0' <= c && c <= '9') || + c == '_' || c == ':' || c == '.' || c == '-') { + NEXT_CHAR(p); + c = *p; + if (c == '\0') break; + } + } + return p; +} + +static TCHAR* SkipXMLComment(TCHAR *p) { + if (p != NULL) { + if (JPACKAGE_STRNCMP(p, _T(""), 3) == 0) { + SKIP_CHARS(p, 3); + return p; + } + NEXT_CHAR(p); + } while (*p != '\0'); + } + } + return p; +} + +static TCHAR* SkipXMLDocType(TCHAR *p) { + if (p != NULL) { + if (JPACKAGE_STRNCMP(p, _T("') { + NEXT_CHAR(p); + return p; + } + NEXT_CHAR(p); + } + } + } + return p; +} + +static TCHAR* SkipXMLProlog(TCHAR *p) { + if (p != NULL) { + if (JPACKAGE_STRNCMP(p, _T(""), 2) == 0) { + SKIP_CHARS(p, 2); + return p; + } + NEXT_CHAR(p); + } while (*p != '\0'); + } + } + return p; +} + +/* Search for the built-in XML entities: + * & (&), < (<), > (>), ' ('), and "e(") + * and convert them to a real TCHARacter + */ +static void ConvertBuiltInEntities(TCHAR* p) { + TCHAR* q; + q = p; + // We are not using NEXT_CHAR() to check if *q is NULL, + // as q is output location and offset for q is smaller than for p. + while (*p) { + if (IsPCData(p)) { + /* dont convert &xxx values within PData */ + TCHAR *end; + end = SkipPCData(p); + while (p < end) { + *q++ = *p; + NEXT_CHAR(p); + } + } else { + if (JPACKAGE_STRNCMP(p, _T("&"), 5) == 0) { + *q++ = '&'; + SKIP_CHARS(p, 5); + } else if (JPACKAGE_STRNCMP(p, _T("<"), 4) == 0) { + *q = '<'; + SKIP_CHARS(p, 4); + } else if (JPACKAGE_STRNCMP(p, _T(">"), 4) == 0) { + *q = '>'; + SKIP_CHARS(p, 4); + } else if (JPACKAGE_STRNCMP(p, _T("'"), 6) == 0) { + *q = '\''; + SKIP_CHARS(p, 6); + } else if (JPACKAGE_STRNCMP(p, _T(""e;"), 7) == 0) { + *q = '\"'; + SKIP_CHARS(p, 7); + } else { + *q++ = *p; + NEXT_CHAR(p); + } + } + } + *q = '\0'; +} + +/* ------------------------------------------------------------- */ +/* XML tokenizer */ + +#define TOKEN_UNKNOWN 0 +#define TOKEN_BEGIN_TAG 1 /* */ +#define TOKEN_EMPTY_CLOSE_BRACKET 4 /* /> */ +#define TOKEN_PCDATA 5 /* pcdata */ +#define TOKEN_CDATA 6 /* cdata */ +#define TOKEN_EOF 7 + +static TCHAR* CurPos = NULL; +static TCHAR* CurTokenName = NULL; +static int CurTokenType; +static int MaxTokenSize = -1; + +/* Copy token from buffer to Token variable */ +static void SetToken(int type, TCHAR* start, TCHAR* end) { + int len = end - start; + if (len > MaxTokenSize) { + if (CurTokenName != NULL) free(CurTokenName); + CurTokenName = (TCHAR *) malloc((len + 1) * sizeof (TCHAR)); + if (CurTokenName == NULL) { + return; + } + MaxTokenSize = len; + } + + CurTokenType = type; + JPACKAGE_STRNCPY(CurTokenName, len + 1, start, len); + CurTokenName[len] = '\0'; +} + +/* Skip XML comments, doctypes, and prolog tags */ +static TCHAR* SkipFilling(void) { + TCHAR *q = CurPos; + + /* Skip white space and comment sections */ + do { + q = CurPos; + CurPos = SkipWhiteSpace(CurPos); + CurPos = SkipXMLComment(CurPos); /* Must be called befor DocTypes */ + CurPos = SkipXMLDocType(CurPos); /* directives */ + CurPos = SkipXMLProlog(CurPos); /* directives */ + } while (CurPos != q); + + return CurPos; +} + +/* Parses next token and initializes the global token variables above + The tokennizer automatically skips comments () and + directives. + */ +static void GetNextToken(void) { + TCHAR *p, *q; + + /* Skip white space and comment sections */ + p = SkipFilling(); + + if (p == NULL || *p == '\0') { + CurTokenType = TOKEN_EOF; + return; + } else if (p[0] == '<' && p[1] == '/') { + /* TOKEN_END_TAG */ + q = SkipXMLName(p + 2); + SetToken(TOKEN_END_TAG, p + 2, q); + p = q; + } else if (*p == '<') { + /* TOKEN_BEGIN_TAG */ + q = SkipXMLName(p + 1); + SetToken(TOKEN_BEGIN_TAG, p + 1, q); + p = q; + } else if (p[0] == '>') { + CurTokenType = TOKEN_CLOSE_BRACKET; + NEXT_CHAR(p); + } else if (p[0] == '/' && p[1] == '>') { + CurTokenType = TOKEN_EMPTY_CLOSE_BRACKET; + SKIP_CHARS(p, 2); + } else { + /* Search for end of data */ + q = p + 1; + while (*q && *q != '<') { + if (IsPCData(q)) { + q = SkipPCData(q); + } else { + NEXT_CHAR(q); + } + } + SetToken(TOKEN_PCDATA, p, q); + /* Convert all entities inside token */ + ConvertBuiltInEntities(CurTokenName); + p = q; + } + /* Advance pointer to beginning of next token */ + CurPos = p; +} + +static XMLNode* CreateXMLNode(int type, TCHAR* name) { + XMLNode* node; + node = (XMLNode*) malloc(sizeof (XMLNode)); + if (node == NULL) { + return NULL; + } + node->_type = type; + node->_name = name; + node->_next = NULL; + node->_sub = NULL; + node->_attributes = NULL; + return node; +} + +static XMLAttribute* CreateXMLAttribute(TCHAR *name, TCHAR* value) { + XMLAttribute* attr; + attr = (XMLAttribute*) malloc(sizeof (XMLAttribute)); + if (attr == NULL) { + return NULL; + } + attr->_name = name; + attr->_value = value; + attr->_next = NULL; + return attr; +} + +XMLNode* ParseXMLDocument(TCHAR* buf) { + XMLNode* root; + int err_code = setjmp(jmpbuf); + switch (err_code) { + case JMP_NO_ERROR: +#ifndef _UNICODE + /* Remove UTF-8 encoding from buffer */ + RemoveNonAsciiUTF8FromBuffer(buf); +#endif + + /* Get first Token */ + CurPos = buf; + GetNextToken(); + + /* Parse document*/ + root = ParseXMLElement(); + break; + case JMP_OUT_OF_RANGE: + /* cleanup: */ + if (root_node != NULL) { + FreeXMLDocument(root_node); + root_node = NULL; + } + if (CurTokenName != NULL) free(CurTokenName); + fprintf(stderr, "Error during parsing jnlp file...\n"); + exit(-1); + break; + default: + root = NULL; + break; + } + + return root; +} + +static XMLNode* ParseXMLElement(void) { + XMLNode* node = NULL; + XMLNode* subnode = NULL; + XMLNode* nextnode = NULL; + XMLAttribute* attr = NULL; + + if (CurTokenType == TOKEN_BEGIN_TAG) { + + /* Create node for new element tag */ + node = CreateXMLNode(xmlTagType, JPACKAGE_STRDUP(CurTokenName)); + /* We need to save root node pointer to be able to cleanup + if an error happens during parsing */ + if (!root_node) { + root_node = node; + } + /* Parse attributes. This section eats a all input until + EOF, a > or a /> */ + attr = ParseXMLAttribute(); + while (attr != NULL) { + attr->_next = node->_attributes; + node->_attributes = attr; + attr = ParseXMLAttribute(); + } + + /* This will eihter be a TOKEN_EOF, TOKEN_CLOSE_BRACKET, or a + * TOKEN_EMPTY_CLOSE_BRACKET */ + GetNextToken(); + + if (CurTokenType == TOKEN_EMPTY_CLOSE_BRACKET) { + GetNextToken(); + /* We are done with the sublevel - fall through to continue */ + /* parsing tags at the same level */ + } else if (CurTokenType == TOKEN_CLOSE_BRACKET) { + GetNextToken(); + + /* Parse until end tag if found */ + node->_sub = ParseXMLElement(); + + if (CurTokenType == TOKEN_END_TAG) { + /* Find closing bracket '>' for end tag */ + do { + GetNextToken(); + } while (CurTokenType != TOKEN_EOF && + CurTokenType != TOKEN_CLOSE_BRACKET); + GetNextToken(); + } + } + + /* Continue parsing rest on same level */ + if (CurTokenType != TOKEN_EOF) { + /* Parse rest of stream at same level */ + node->_next = ParseXMLElement(); + } + return node; + + } else if (CurTokenType == TOKEN_PCDATA) { + /* Create node for pcdata */ + node = CreateXMLNode(xmlPCDataType, JPACKAGE_STRDUP(CurTokenName)); + /* We need to save root node pointer to be able to cleanup + if an error happens during parsing */ + if (!root_node) { + root_node = node; + } + GetNextToken(); + return node; + } + + /* Something went wrong. */ + return NULL; +} + +/* Parses an XML attribute. */ +static XMLAttribute* ParseXMLAttribute(void) { + TCHAR* q = NULL; + TCHAR* name = NULL; + TCHAR* PrevPos = NULL; + + do { + /* We need to check this condition to avoid endless loop + in case if an error happend during parsing. */ + if (PrevPos == CurPos) { + if (name != NULL) { + free(name); + name = NULL; + } + + return NULL; + } + + PrevPos = CurPos; + + /* Skip whitespace etc. */ + SkipFilling(); + + /* Check if we are done witht this attribute section */ + if (CurPos[0] == '\0' || + CurPos[0] == '>' || + (CurPos[0] == '/' && CurPos[1] == '>')) { + + if (name != NULL) { + free(name); + name = NULL; + } + + return NULL; + } + + /* Find end of name */ + q = CurPos; + while (*q && !iswspace(*q) && *q != '=') NEXT_CHAR(q); + + SetToken(TOKEN_UNKNOWN, CurPos, q); + if (name) { + free(name); + name = NULL; + } + name = JPACKAGE_STRDUP(CurTokenName); + + /* Skip any whitespace */ + CurPos = q; + CurPos = SkipFilling(); + + /* Next TCHARacter must be '=' for a valid attribute. + If it is not, this is really an error. + We ignore this, and just try to parse an attribute + out of the rest of the string. + */ + } while (*CurPos != '='); + + NEXT_CHAR(CurPos); + CurPos = SkipWhiteSpace(CurPos); + /* Parse CDATA part of attribute */ + if ((*CurPos == '\"') || (*CurPos == '\'')) { + TCHAR quoteChar = *CurPos; + q = ++CurPos; + while (*q != '\0' && *q != quoteChar) NEXT_CHAR(q); + SetToken(TOKEN_CDATA, CurPos, q); + CurPos = q + 1; + } else { + q = CurPos; + while (*q != '\0' && !iswspace(*q)) NEXT_CHAR(q); + SetToken(TOKEN_CDATA, CurPos, q); + CurPos = q; + } + + //Note: no need to free name and CurTokenName duplicate; they're assigned + // to an XMLAttribute structure in CreateXMLAttribute + + return CreateXMLAttribute(name, JPACKAGE_STRDUP(CurTokenName)); +} + +void FreeXMLDocument(XMLNode* root) { + if (root == NULL) return; + FreeXMLDocument(root->_sub); + FreeXMLDocument(root->_next); + FreeXMLAttribute(root->_attributes); + free(root->_name); + free(root); +} + +static void FreeXMLAttribute(XMLAttribute* attr) { + if (attr == NULL) return; + free(attr->_name); + free(attr->_value); + FreeXMLAttribute(attr->_next); + free(attr); +} + +/* Find element at current level with a given name */ +XMLNode* FindXMLChild(XMLNode* root, const TCHAR* name) { + if (root == NULL) return NULL; + + if (root->_type == xmlTagType && JPACKAGE_STRCMP(root->_name, name) == 0) { + return root; + } + + return FindXMLChild(root->_next, name); +} + +/* Search for an attribute with the given name and returns the contents. + * Returns NULL if attribute is not found + */ +TCHAR* FindXMLAttribute(XMLAttribute* attr, const TCHAR* name) { + if (attr == NULL) return NULL; + if (JPACKAGE_STRCMP(attr->_name, name) == 0) return attr->_value; + return FindXMLAttribute(attr->_next, name); +} + +void PrintXMLDocument(XMLNode* node, int indt) { + if (node == NULL) return; + + if (node->_type == xmlTagType) { + JPACKAGE_PRINTF(_T("\n")); + indent(indt); + JPACKAGE_PRINTF(_T("<%s"), node->_name); + PrintXMLAttributes(node->_attributes); + if (node->_sub == NULL) { + JPACKAGE_PRINTF(_T("/>\n")); + } else { + JPACKAGE_PRINTF(_T(">")); + PrintXMLDocument(node->_sub, indt + 1); + indent(indt); + JPACKAGE_PRINTF(_T(""), node->_name); + } + } else { + JPACKAGE_PRINTF(_T("%s"), node->_name); + } + PrintXMLDocument(node->_next, indt); +} + +static void PrintXMLAttributes(XMLAttribute* attr) { + if (attr == NULL) return; + + JPACKAGE_PRINTF(_T(" %s=\"%s\""), attr->_name, attr->_value); + PrintXMLAttributes(attr->_next); +} + +static void indent(int indt) { + int i; + for (i = 0; i < indt; i++) { + JPACKAGE_PRINTF(_T(" ")); + } +} + +const TCHAR *CDStart = _T(""); + +static TCHAR* SkipPCData(TCHAR *p) { + TCHAR *end = JPACKAGE_STRSTR(p, CDEnd); + if (end != NULL) { + return end + sizeof (CDEnd); + } + return (++p); +} + +static int IsPCData(TCHAR *p) { + const int size = sizeof (CDStart); + return (JPACKAGE_STRNCMP(CDStart, p, size) == 0); +} diff --git a/src/jdk.incubator.jpackage/linux/native/libapplauncher/LinuxPlatform.h b/src/jdk.incubator.jpackage/linux/native/libapplauncher/LinuxPlatform.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/native/libapplauncher/LinuxPlatform.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef LINUXPLATFORM_H +#define LINUXPLATFORM_H + +#include "Platform.h" +#include "PosixPlatform.h" +#include +#include +#include +#include + +class LinuxPlatform : virtual public Platform, PosixPlatform { +private: + pthread_t FMainThread; + +protected: + virtual TString getTmpDirString(); + +public: + LinuxPlatform(void); + virtual ~LinuxPlatform(void); + + TString GetPackageAppDirectory(); + TString GetPackageLauncherDirectory(); + TString GetPackageRuntimeBinDirectory(); + + virtual void ShowMessage(TString title, TString description); + virtual void ShowMessage(TString description); + + virtual TCHAR* ConvertStringToFileSystemString( + TCHAR* Source, bool &release); + virtual TCHAR* ConvertFileSystemStringToString( + TCHAR* Source, bool &release); + + virtual TString GetPackageRootDirectory(); + virtual TString GetAppDataDirectory(); + virtual TString GetAppName(); + + virtual TString GetModuleFileName(); + + virtual TString GetBundledJavaLibraryFileName(TString RuntimePath); + + virtual ISectionalPropertyContainer* GetConfigFile(TString FileName); + + virtual bool IsMainThread(); + virtual TPlatformNumber GetMemorySize(); +}; + +#endif //LINUXPLATFORM_H diff --git a/src/jdk.incubator.jpackage/linux/native/libapplauncher/PlatformDefs.h b/src/jdk.incubator.jpackage/linux/native/libapplauncher/PlatformDefs.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/linux/native/libapplauncher/PlatformDefs.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef PLATFORM_DEFS_H +#define PLATFORM_DEFS_H + +#include +#include +#include +#include +#include +#include + +using namespace std; + +#ifndef LINUX +#define LINUX +#endif + +#define _T(x) x + +typedef char TCHAR; +typedef std::string TString; +#define StringLength strlen + +typedef unsigned long DWORD; + +#define TRAILING_PATHSEPARATOR '/' +#define BAD_TRAILING_PATHSEPARATOR '\\' +#define PATH_SEPARATOR ':' +#define BAD_PATH_SEPARATOR ';' +#define MAX_PATH 1000 + +typedef long TPlatformNumber; +typedef pid_t TProcessID; + +#define HMODULE void* + +typedef void* Module; +typedef void* Procedure; + +#define StringToFileSystemString PlatformString +#define FileSystemStringToString PlatformString + +#endif // PLATFORM_DEFS_H diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/EnumeratedBundlerParam.java b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/EnumeratedBundlerParam.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/EnumeratedBundlerParam.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * EnumeratedBundlerParams + * + * Contains key-value pairs (elements) where keys are "displayable" + * keys which the IDE can display/choose and values are "identifier" values + * which can be stored in parameters' map. + * + * For instance the Mac has a predefined set of categories which can be applied + * to LSApplicationCategoryType which is required for the mac app store. + * + * The following example illustrates a simple usage of + * the MAC_CATEGORY parameter: + * + *
{@code
+ *     Set keys = MAC_CATEGORY.getDisplayableKeys();
+ *
+ *     String key = getLastValue(keys); // get last value for example
+ *
+ *     String value = MAC_CATEGORY.getValueForDisplayableKey(key);
+ *     params.put(MAC_CATEGORY.getID(), value);
+ * }
+ * + */ +class EnumeratedBundlerParam extends BundlerParamInfo { + // Not sure if this is the correct order, my idea is that from IDE + // perspective the string to display to the user is the key and then the + // value is some type of object (although probably a String in most cases) + private final Map elements; + private final boolean strict; + + EnumeratedBundlerParam(String id, Class valueType, + Function, T> defaultValueFunction, + BiFunction, T> stringConverter, + Map elements, boolean strict) { + this.id = id; + this.valueType = valueType; + this.defaultValueFunction = defaultValueFunction; + this.stringConverter = stringConverter; + this.elements = elements; + this.strict = strict; + } + + boolean isInPossibleValues(T value) { + return elements.values().contains(value); + } + + // Having the displayable values as the keys seems a bit wacky + Set getDisplayableKeys() { + return Collections.unmodifiableSet(elements.keySet()); + } + + // mapping from a "displayable" key to an "identifier" value. + T getValueForDisplayableKey(String displayableKey) { + return elements.get(displayableKey); + } + + boolean isStrict() { + return strict; + } + + boolean isLoose() { + return !isStrict(); + } + +} diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppBundler.java b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppBundler.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.ResourceBundle; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; +import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.*; + +public class MacAppBundler extends AbstractImageBundler { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MacResources"); + + private static final String TEMPLATE_BUNDLE_ICON = "java.icns"; + + public static final BundlerParamInfo MAC_CF_BUNDLE_NAME = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(), + String.class, + params -> null, + (s, p) -> s); + + public static final BundlerParamInfo MAC_CF_BUNDLE_VERSION = + new StandardBundlerParam<>( + "mac.CFBundleVersion", + String.class, + p -> { + String s = VERSION.fetchFrom(p); + if (validCFBundleVersion(s)) { + return s; + } else { + return "100"; + } + }, + (s, p) -> s); + + public static final BundlerParamInfo DEFAULT_ICNS_ICON = + new StandardBundlerParam<>( + ".mac.default.icns", + String.class, + params -> TEMPLATE_BUNDLE_ICON, + (s, p) -> s); + + public static final BundlerParamInfo DEVELOPER_ID_APP_SIGNING_KEY = + new StandardBundlerParam<>( + "mac.signing-key-developer-id-app", + String.class, + params -> { + String result = MacBaseInstallerBundler.findKey( + "Developer ID Application: " + + SIGNING_KEY_USER.fetchFrom(params), + SIGNING_KEYCHAIN.fetchFrom(params), + VERBOSE.fetchFrom(params)); + if (result != null) { + MacCertificate certificate = new MacCertificate(result); + + if (!certificate.isValid()) { + Log.error(MessageFormat.format(I18N.getString( + "error.certificate.expired"), result)); + } + } + + return result; + }, + (s, p) -> s); + + public static final BundlerParamInfo BUNDLE_ID_SIGNING_PREFIX = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAC_BUNDLE_SIGNING_PREFIX.getId(), + String.class, + params -> IDENTIFIER.fetchFrom(params) + ".", + (s, p) -> s); + + public static final BundlerParamInfo ICON_ICNS = + new StandardBundlerParam<>( + "icon.icns", + File.class, + params -> { + File f = ICON.fetchFrom(params); + if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { + Log.error(MessageFormat.format( + I18N.getString("message.icon-not-icns"), f)); + return null; + } + return f; + }, + (s, p) -> new File(s)); + + public static boolean validCFBundleVersion(String v) { + // CFBundleVersion (String - iOS, OS X) specifies the build version + // number of the bundle, which identifies an iteration (released or + // unreleased) of the bundle. The build version number should be a + // string comprised of three non-negative, period-separated integers + // with the first integer being greater than zero. The string should + // only contain numeric (0-9) and period (.) characters. Leading zeros + // are truncated from each integer and will be ignored (that is, + // 1.02.3 is equivalent to 1.2.3). This key is not localizable. + + if (v == null) { + return false; + } + + String p[] = v.split("\\."); + if (p.length > 3 || p.length < 1) { + Log.verbose(I18N.getString( + "message.version-string-too-many-components")); + return false; + } + + try { + BigInteger n = new BigInteger(p[0]); + if (BigInteger.ONE.compareTo(n) > 0) { + Log.verbose(I18N.getString( + "message.version-string-first-number-not-zero")); + return false; + } + if (p.length > 1) { + n = new BigInteger(p[1]); + if (BigInteger.ZERO.compareTo(n) > 0) { + Log.verbose(I18N.getString( + "message.version-string-no-negative-numbers")); + return false; + } + } + if (p.length > 2) { + n = new BigInteger(p[2]); + if (BigInteger.ZERO.compareTo(n) > 0) { + Log.verbose(I18N.getString( + "message.version-string-no-negative-numbers")); + return false; + } + } + } catch (NumberFormatException ne) { + Log.verbose(I18N.getString("message.version-string-numbers-only")); + Log.verbose(ne); + return false; + } + + return true; + } + + @Override + public boolean validate(Map params) + throws ConfigException { + try { + return doValidate(params); + } catch (RuntimeException re) { + if (re.getCause() instanceof ConfigException) { + throw (ConfigException) re.getCause(); + } else { + throw new ConfigException(re); + } + } + } + + private boolean doValidate(Map params) + throws ConfigException { + + imageBundleValidation(params); + + if (StandardBundlerParam.getPredefinedAppImage(params) != null) { + return true; + } + + // validate short version + if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(params))) { + throw new ConfigException( + I18N.getString("error.invalid-cfbundle-version"), + I18N.getString("error.invalid-cfbundle-version.advice")); + } + + // reject explicitly set sign to true and no valid signature key + if (Optional.ofNullable(MacAppImageBuilder. + SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) { + String signingIdentity = + DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params); + if (signingIdentity == null) { + throw new ConfigException( + I18N.getString("error.explicit-sign-no-cert"), + I18N.getString("error.explicit-sign-no-cert.advice")); + } + + // Signing will not work without Xcode with command line developer tools + try { + ProcessBuilder pb = new ProcessBuilder("xcrun", "--help"); + Process p = pb.start(); + int code = p.waitFor(); + if (code != 0) { + throw new ConfigException( + I18N.getString("error.no.xcode.signing"), + I18N.getString("error.no.xcode.signing.advice")); + } + } catch (IOException | InterruptedException ex) { + throw new ConfigException(ex); + } + } + + return true; + } + + File doBundle(Map params, File outputDirectory, + boolean dependentTask) throws PackagerException { + if (StandardBundlerParam.isRuntimeInstaller(params)) { + return PREDEFINED_RUNTIME_IMAGE.fetchFrom(params); + } else { + return doAppBundle(params, outputDirectory, dependentTask); + } + } + + File doAppBundle(Map params, File outputDirectory, + boolean dependentTask) throws PackagerException { + try { + File rootDirectory = createRoot(params, outputDirectory, + dependentTask, APP_NAME.fetchFrom(params) + ".app"); + AbstractAppImageBuilder appBuilder = + new MacAppImageBuilder(params, outputDirectory.toPath()); + if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(params) == null ) { + JLinkBundlerHelper.execute(params, appBuilder); + } else { + StandardBundlerParam.copyPredefinedRuntimeImage( + params, appBuilder); + } + return rootDirectory; + } catch (PackagerException pe) { + throw pe; + } catch (Exception ex) { + Log.verbose(ex); + throw new PackagerException(ex); + } + } + + ///////////////////////////////////////////////////////////////////////// + // Implement Bundler + ///////////////////////////////////////////////////////////////////////// + + @Override + public String getName() { + return I18N.getString("app.bundler.name"); + } + + @Override + public String getID() { + return "mac.app"; + } + + @Override + public String getBundleType() { + return "IMAGE"; + } + + @Override + public File execute(Map params, + File outputParentDir) throws PackagerException { + return doBundle(params, outputParentDir, false); + } + + @Override + public boolean supported(boolean runtimeInstaller) { + return true; + } + + @Override + public boolean isDefault() { + return false; + } + +} diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppImageBuilder.java b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppImageBuilder.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppImageBuilder.java @@ -0,0 +1,945 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; +import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.*; +import static jdk.incubator.jpackage.internal.MacAppBundler.*; +import static jdk.incubator.jpackage.internal.OverridableResource.createResource; + +public class MacAppImageBuilder extends AbstractAppImageBuilder { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MacResources"); + + private static final String LIBRARY_NAME = "libapplauncher.dylib"; + private static final String TEMPLATE_BUNDLE_ICON = "java.icns"; + private static final String OS_TYPE_CODE = "APPL"; + private static final String TEMPLATE_INFO_PLIST_LITE = + "Info-lite.plist.template"; + private static final String TEMPLATE_RUNTIME_INFO_PLIST = + "Runtime-Info.plist.template"; + + private final Path root; + private final Path contentsDir; + private final Path appDir; + private final Path javaModsDir; + private final Path resourcesDir; + private final Path macOSDir; + private final Path runtimeDir; + private final Path runtimeRoot; + private final Path mdir; + + private static List keyChains; + + public static final BundlerParamInfo + MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>( + "mac.configure-launcher-in-plist", + Boolean.class, + params -> Boolean.FALSE, + (s, p) -> Boolean.valueOf(s)); + + public static final BundlerParamInfo MAC_CF_BUNDLE_NAME = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(), + String.class, + params -> null, + (s, p) -> s); + + public static final BundlerParamInfo MAC_CF_BUNDLE_IDENTIFIER = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(), + String.class, + params -> { + // Get identifier from app image if user provided + // app image and did not provide the identifier via CLI. + String identifier = extractBundleIdentifier(params); + if (identifier != null) { + return identifier; + } + + return IDENTIFIER.fetchFrom(params); + }, + (s, p) -> s); + + public static final BundlerParamInfo MAC_CF_BUNDLE_VERSION = + new StandardBundlerParam<>( + "mac.CFBundleVersion", + String.class, + p -> { + String s = VERSION.fetchFrom(p); + if (validCFBundleVersion(s)) { + return s; + } else { + return "100"; + } + }, + (s, p) -> s); + + public static final BundlerParamInfo ICON_ICNS = + new StandardBundlerParam<>( + "icon.icns", + File.class, + params -> { + File f = ICON.fetchFrom(params); + if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { + Log.error(MessageFormat.format( + I18N.getString("message.icon-not-icns"), f)); + return null; + } + return f; + }, + (s, p) -> new File(s)); + + public static final StandardBundlerParam SIGN_BUNDLE = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAC_SIGN.getId(), + Boolean.class, + params -> false, + // valueOf(null) is false, we actually do want null in some cases + (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? + null : Boolean.valueOf(s) + ); + + public MacAppImageBuilder(Map params, Path imageOutDir) + throws IOException { + super(params, imageOutDir.resolve(APP_NAME.fetchFrom(params) + + ".app/Contents/runtime/Contents/Home")); + + Objects.requireNonNull(imageOutDir); + + this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app"); + this.contentsDir = root.resolve("Contents"); + this.appDir = contentsDir.resolve("app"); + this.javaModsDir = appDir.resolve("mods"); + this.resourcesDir = contentsDir.resolve("Resources"); + this.macOSDir = contentsDir.resolve("MacOS"); + this.runtimeDir = contentsDir.resolve("runtime"); + this.runtimeRoot = runtimeDir.resolve("Contents/Home"); + this.mdir = runtimeRoot.resolve("lib"); + Files.createDirectories(appDir); + Files.createDirectories(resourcesDir); + Files.createDirectories(macOSDir); + Files.createDirectories(runtimeDir); + } + + private void writeEntry(InputStream in, Path dstFile) throws IOException { + Files.createDirectories(dstFile.getParent()); + Files.copy(in, dstFile); + } + + public static boolean validCFBundleVersion(String v) { + // CFBundleVersion (String - iOS, OS X) specifies the build version + // number of the bundle, which identifies an iteration (released or + // unreleased) of the bundle. The build version number should be a + // string comprised of three non-negative, period-separated integers + // with the first integer being greater than zero. The string should + // only contain numeric (0-9) and period (.) characters. Leading zeros + // are truncated from each integer and will be ignored (that is, + // 1.02.3 is equivalent to 1.2.3). This key is not localizable. + + if (v == null) { + return false; + } + + String p[] = v.split("\\."); + if (p.length > 3 || p.length < 1) { + Log.verbose(I18N.getString( + "message.version-string-too-many-components")); + return false; + } + + try { + BigInteger n = new BigInteger(p[0]); + if (BigInteger.ONE.compareTo(n) > 0) { + Log.verbose(I18N.getString( + "message.version-string-first-number-not-zero")); + return false; + } + if (p.length > 1) { + n = new BigInteger(p[1]); + if (BigInteger.ZERO.compareTo(n) > 0) { + Log.verbose(I18N.getString( + "message.version-string-no-negative-numbers")); + return false; + } + } + if (p.length > 2) { + n = new BigInteger(p[2]); + if (BigInteger.ZERO.compareTo(n) > 0) { + Log.verbose(I18N.getString( + "message.version-string-no-negative-numbers")); + return false; + } + } + } catch (NumberFormatException ne) { + Log.verbose(I18N.getString("message.version-string-numbers-only")); + Log.verbose(ne); + return false; + } + + return true; + } + + @Override + public Path getAppDir() { + return appDir; + } + + @Override + public Path getAppModsDir() { + return javaModsDir; + } + + @Override + public void prepareApplicationFiles(Map params) + throws IOException { + Map originalParams = new HashMap<>(params); + // Generate PkgInfo + File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo"); + pkgInfoFile.createNewFile(); + writePkgInfo(pkgInfoFile); + + Path executable = macOSDir.resolve(getLauncherName(params)); + + // create the main app launcher + try (InputStream is_launcher = + getResourceAsStream("jpackageapplauncher"); + InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) { + // Copy executable and library to MacOS folder + writeEntry(is_launcher, executable); + writeEntry(is_lib, macOSDir.resolve(LIBRARY_NAME)); + } + executable.toFile().setExecutable(true, false); + // generate main app launcher config file + File cfg = new File(root.toFile(), getLauncherCfgName(params)); + writeCfgFile(params, cfg); + + // create additional app launcher(s) and config file(s) + List> entryPoints = + StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params); + for (Map entryPoint : entryPoints) { + Map tmp = + AddLauncherArguments.merge(originalParams, entryPoint); + + // add executable for add launcher + Path addExecutable = macOSDir.resolve(getLauncherName(tmp)); + try (InputStream is = getResourceAsStream("jpackageapplauncher");) { + writeEntry(is, addExecutable); + } + addExecutable.toFile().setExecutable(true, false); + + // add config file for add launcher + cfg = new File(root.toFile(), getLauncherCfgName(tmp)); + writeCfgFile(tmp, cfg); + } + + // Copy class path entries to Java folder + copyClassPathEntries(appDir, params); + + /*********** Take care of "config" files *******/ + + createResource(TEMPLATE_BUNDLE_ICON, params) + .setCategory("icon") + .setExternal(ICON_ICNS.fetchFrom(params)) + .saveToFile(resourcesDir.resolve(APP_NAME.fetchFrom(params) + + ".icns")); + + // copy file association icons + for (Map fa : FILE_ASSOCIATIONS.fetchFrom(params)) { + File f = FA_ICON.fetchFrom(fa); + if (f != null && f.exists()) { + try (InputStream in2 = new FileInputStream(f)) { + Files.copy(in2, resourcesDir.resolve(f.getName())); + } + + } + } + + copyRuntimeFiles(params); + sign(params); + } + + @Override + public void prepareJreFiles(Map params) + throws IOException { + copyRuntimeFiles(params); + sign(params); + } + + @Override + File getRuntimeImageDir(File runtimeImageTop) { + File home = new File(runtimeImageTop, "Contents/Home"); + return (home.exists() ? home : runtimeImageTop); + } + + private void copyRuntimeFiles(Map params) + throws IOException { + // Generate Info.plist + writeInfoPlist(contentsDir.resolve("Info.plist").toFile(), params); + + // generate java runtime info.plist + writeRuntimeInfoPlist( + runtimeDir.resolve("Contents/Info.plist").toFile(), params); + + // copy library + Path runtimeMacOSDir = Files.createDirectories( + runtimeDir.resolve("Contents/MacOS")); + + // JDK 9, 10, and 11 have extra '/jli/' subdir + Path jli = runtimeRoot.resolve("lib/libjli.dylib"); + if (!Files.exists(jli)) { + jli = runtimeRoot.resolve("lib/jli/libjli.dylib"); + } + + Files.copy(jli, runtimeMacOSDir.resolve("libjli.dylib")); + } + + private void sign(Map params) throws IOException { + if (Optional.ofNullable( + SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { + try { + addNewKeychain(params); + } catch (InterruptedException e) { + Log.error(e.getMessage()); + } + String signingIdentity = + DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params); + if (signingIdentity != null) { + signAppBundle(params, root, signingIdentity, + BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), null, null); + } + restoreKeychainList(params); + } + } + + private String getLauncherName(Map params) { + if (APP_NAME.fetchFrom(params) != null) { + return APP_NAME.fetchFrom(params); + } else { + return MAIN_CLASS.fetchFrom(params); + } + } + + public static String getLauncherCfgName( + Map params) { + return "Contents/app/" + APP_NAME.fetchFrom(params) + ".cfg"; + } + + private void copyClassPathEntries(Path javaDirectory, + Map params) throws IOException { + List resourcesList = + APP_RESOURCES_LIST.fetchFrom(params); + if (resourcesList == null) { + throw new RuntimeException( + I18N.getString("message.null-classpath")); + } + + for (RelativeFileSet classPath : resourcesList) { + File srcdir = classPath.getBaseDirectory(); + for (String fname : classPath.getIncludedFiles()) { + copyEntry(javaDirectory, srcdir, fname); + } + } + } + + private String getBundleName(Map params) { + if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) { + String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params); + if (bn.length() > 16) { + Log.error(MessageFormat.format(I18N.getString( + "message.bundle-name-too-long-warning"), + MAC_CF_BUNDLE_NAME.getID(), bn)); + } + return MAC_CF_BUNDLE_NAME.fetchFrom(params); + } else if (APP_NAME.fetchFrom(params) != null) { + return APP_NAME.fetchFrom(params); + } else { + String nm = MAIN_CLASS.fetchFrom(params); + if (nm.length() > 16) { + nm = nm.substring(0, 16); + } + return nm; + } + } + + private void writeRuntimeInfoPlist(File file, + Map params) throws IOException { + Map data = new HashMap<>(); + String identifier = StandardBundlerParam.isRuntimeInstaller(params) ? + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) : + "com.oracle.java." + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params); + data.put("CF_BUNDLE_IDENTIFIER", identifier); + String name = StandardBundlerParam.isRuntimeInstaller(params) ? + getBundleName(params): "Java Runtime Image"; + data.put("CF_BUNDLE_NAME", name); + data.put("CF_BUNDLE_VERSION", VERSION.fetchFrom(params)); + data.put("CF_BUNDLE_SHORT_VERSION_STRING", VERSION.fetchFrom(params)); + + createResource(TEMPLATE_RUNTIME_INFO_PLIST, params) + .setPublicName("Runtime-Info.plist") + .setCategory(I18N.getString("resource.runtime-info-plist")) + .setSubstitutionData(data) + .saveToFile(file); + } + + private void writeInfoPlist(File file, Map params) + throws IOException { + Log.verbose(MessageFormat.format(I18N.getString( + "message.preparing-info-plist"), file.getAbsolutePath())); + + //prepare config for exe + //Note: do not need CFBundleDisplayName if we don't support localization + Map data = new HashMap<>(); + data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns"); + data.put("DEPLOY_BUNDLE_IDENTIFIER", + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); + data.put("DEPLOY_BUNDLE_NAME", + getBundleName(params)); + data.put("DEPLOY_BUNDLE_COPYRIGHT", + COPYRIGHT.fetchFrom(params) != null ? + COPYRIGHT.fetchFrom(params) : "Unknown"); + data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params)); + data.put("DEPLOY_BUNDLE_SHORT_VERSION", + VERSION.fetchFrom(params) != null ? + VERSION.fetchFrom(params) : "1.0.0"); + data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", + MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? + MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100"); + + boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null; + boolean hasMainModule = + StandardBundlerParam.MODULE.fetchFrom(params) != null; + + if (hasMainJar) { + data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params). + getIncludedFiles().iterator().next()); + } + else if (hasMainModule) { + data.put("DEPLOY_MODULE_NAME", + StandardBundlerParam.MODULE.fetchFrom(params)); + } + + StringBuilder sb = new StringBuilder(); + List jvmOptions = JAVA_OPTIONS.fetchFrom(params); + + String newline = ""; //So we don't add extra line after last append + for (String o : jvmOptions) { + sb.append(newline).append( + " ").append(o).append(""); + newline = "\n"; + } + + data.put("DEPLOY_JAVA_OPTIONS", sb.toString()); + + sb = new StringBuilder(); + List args = ARGUMENTS.fetchFrom(params); + newline = ""; + // So we don't add unneccessary extra line after last append + + for (String o : args) { + sb.append(newline).append(" ").append(o).append( + ""); + newline = "\n"; + } + data.put("DEPLOY_ARGUMENTS", sb.toString()); + + newline = ""; + + data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params)); + + data.put("DEPLOY_APP_CLASSPATH", + getCfgClassPath(CLASSPATH.fetchFrom(params))); + + StringBuilder bundleDocumentTypes = new StringBuilder(); + StringBuilder exportedTypes = new StringBuilder(); + for (Map + fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) { + + List extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); + + if (extensions == null) { + Log.verbose(I18N.getString( + "message.creating-association-with-null-extension")); + } + + List mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation); + String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + + "." + ((extensions == null || extensions.isEmpty()) + ? "mime" : extensions.get(0)); + String description = FA_DESCRIPTION.fetchFrom(fileAssociation); + File icon = FA_ICON.fetchFrom(fileAssociation); + + bundleDocumentTypes.append(" \n") + .append(" LSItemContentTypes\n") + .append(" \n") + .append(" ") + .append(itemContentType) + .append("\n") + .append(" \n") + .append("\n") + .append(" CFBundleTypeName\n") + .append(" ") + .append(description) + .append("\n") + .append("\n") + .append(" LSHandlerRank\n") + .append(" Owner\n") + // TODO make a bundler arg + .append("\n") + .append(" CFBundleTypeRole\n") + .append(" Editor\n") + // TODO make a bundler arg + .append("\n") + .append(" LSIsAppleDefaultForType\n") + .append(" \n") + // TODO make a bundler arg + .append("\n"); + + if (icon != null && icon.exists()) { + bundleDocumentTypes + .append(" CFBundleTypeIconFile\n") + .append(" ") + .append(icon.getName()) + .append("\n"); + } + bundleDocumentTypes.append(" \n"); + + exportedTypes.append(" \n") + .append(" UTTypeIdentifier\n") + .append(" ") + .append(itemContentType) + .append("\n") + .append("\n") + .append(" UTTypeDescription\n") + .append(" ") + .append(description) + .append("\n") + .append(" UTTypeConformsTo\n") + .append(" \n") + .append(" public.data\n") + //TODO expose this? + .append(" \n") + .append("\n"); + + if (icon != null && icon.exists()) { + exportedTypes.append(" UTTypeIconFile\n") + .append(" ") + .append(icon.getName()) + .append("\n") + .append("\n"); + } + + exportedTypes.append("\n") + .append(" UTTypeTagSpecification\n") + .append(" \n") + // TODO expose via param? .append( + // " com.apple.ostype\n"); + // TODO expose via param? .append( + // " ABCD\n") + .append("\n"); + + if (extensions != null && !extensions.isEmpty()) { + exportedTypes.append( + " public.filename-extension\n") + .append(" \n"); + + for (String ext : extensions) { + exportedTypes.append(" ") + .append(ext) + .append("\n"); + } + exportedTypes.append(" \n"); + } + if (mimeTypes != null && !mimeTypes.isEmpty()) { + exportedTypes.append(" public.mime-type\n") + .append(" \n"); + + for (String mime : mimeTypes) { + exportedTypes.append(" ") + .append(mime) + .append("\n"); + } + exportedTypes.append(" \n"); + } + exportedTypes.append(" \n") + .append(" \n"); + } + String associationData; + if (bundleDocumentTypes.length() > 0) { + associationData = + "\n CFBundleDocumentTypes\n \n" + + bundleDocumentTypes.toString() + + " \n\n" + + " UTExportedTypeDeclarations\n \n" + + exportedTypes.toString() + + " \n"; + } else { + associationData = ""; + } + data.put("DEPLOY_FILE_ASSOCIATIONS", associationData); + + createResource(TEMPLATE_INFO_PLIST_LITE, params) + .setCategory(I18N.getString("resource.app-info-plist")) + .setSubstitutionData(data) + .setPublicName("Info.plist") + .saveToFile(file); + } + + private void writePkgInfo(File file) throws IOException { + //hardcoded as it does not seem we need to change it ever + String signature = "????"; + + try (Writer out = Files.newBufferedWriter(file.toPath())) { + out.write(OS_TYPE_CODE + signature); + out.flush(); + } + } + + public static void addNewKeychain(Map params) + throws IOException, InterruptedException { + if (Platform.getMajorVersion() < 10 || + (Platform.getMajorVersion() == 10 && + Platform.getMinorVersion() < 12)) { + // we need this for OS X 10.12+ + return; + } + + String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); + if (keyChain == null || keyChain.isEmpty()) { + return; + } + + // get current keychain list + String keyChainPath = new File (keyChain).getAbsolutePath().toString(); + List keychainList = new ArrayList<>(); + int ret = IOUtils.getProcessOutput( + keychainList, "security", "list-keychains"); + if (ret != 0) { + Log.error(I18N.getString("message.keychain.error")); + return; + } + + boolean contains = keychainList.stream().anyMatch( + str -> str.trim().equals("\""+keyChainPath.trim()+"\"")); + if (contains) { + // keychain is already added in the search list + return; + } + + keyChains = new ArrayList<>(); + // remove " + keychainList.forEach((String s) -> { + String path = s.trim(); + if (path.startsWith("\"") && path.endsWith("\"")) { + path = path.substring(1, path.length()-1); + } + keyChains.add(path); + }); + + List args = new ArrayList<>(); + args.add("security"); + args.add("list-keychains"); + args.add("-s"); + + args.addAll(keyChains); + args.add(keyChain); + + ProcessBuilder pb = new ProcessBuilder(args); + IOUtils.exec(pb); + } + + public static void restoreKeychainList(Map params) + throws IOException{ + if (Platform.getMajorVersion() < 10 || + (Platform.getMajorVersion() == 10 && + Platform.getMinorVersion() < 12)) { + // we need this for OS X 10.12+ + return; + } + + if (keyChains == null || keyChains.isEmpty()) { + return; + } + + List args = new ArrayList<>(); + args.add("security"); + args.add("list-keychains"); + args.add("-s"); + + args.addAll(keyChains); + + ProcessBuilder pb = new ProcessBuilder(args); + IOUtils.exec(pb); + } + + public static void signAppBundle( + Map params, Path appLocation, + String signingIdentity, String identifierPrefix, + String entitlementsFile, String inheritedEntitlements) + throws IOException { + AtomicReference toThrow = new AtomicReference<>(); + String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params); + String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); + + // sign all dylibs and jars + try (Stream stream = Files.walk(appLocation)) { + stream.peek(path -> { // fix permissions + try { + Set pfp = + Files.getPosixFilePermissions(path); + if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) { + pfp = EnumSet.copyOf(pfp); + pfp.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(path, pfp); + } + } catch (IOException e) { + Log.verbose(e); + } + }).filter(p -> Files.isRegularFile(p) + && !(p.toString().contains("/Contents/MacOS/libjli.dylib") + || p.toString().endsWith(appExecutable) + || p.toString().contains("/Contents/runtime") + || p.toString().contains("/Contents/Frameworks"))).forEach(p -> { + //noinspection ThrowableResultOfMethodCallIgnored + if (toThrow.get() != null) return; + + // If p is a symlink then skip the signing process. + if (Files.isSymbolicLink(p)) { + if (VERBOSE.fetchFrom(params)) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.ignoring.symlink"), p.toString())); + } + } else { + if (p.toString().endsWith(LIBRARY_NAME)) { + if (isFileSigned(p)) { + return; + } + } + + List args = new ArrayList<>(); + args.addAll(Arrays.asList("codesign", + "-s", signingIdentity, // sign with this key + "--prefix", identifierPrefix, + // use the identifier as a prefix + "-vvvv")); + if (entitlementsFile != null && + (p.toString().endsWith(".jar") + || p.toString().endsWith(".dylib"))) { + args.add("--entitlements"); + args.add(entitlementsFile); // entitlements + } else if (inheritedEntitlements != null && + Files.isExecutable(p)) { + args.add("--entitlements"); + args.add(inheritedEntitlements); + // inherited entitlements for executable processes + } + if (keyChain != null && !keyChain.isEmpty()) { + args.add("--keychain"); + args.add(keyChain); + } + args.add(p.toString()); + + try { + Set oldPermissions = + Files.getPosixFilePermissions(p); + File f = p.toFile(); + f.setWritable(true, true); + + ProcessBuilder pb = new ProcessBuilder(args); + IOUtils.exec(pb); + + Files.setPosixFilePermissions(p, oldPermissions); + } catch (IOException ioe) { + toThrow.set(ioe); + } + } + }); + } + IOException ioe = toThrow.get(); + if (ioe != null) { + throw ioe; + } + + // sign all runtime and frameworks + Consumer signIdentifiedByPList = path -> { + //noinspection ThrowableResultOfMethodCallIgnored + if (toThrow.get() != null) return; + + try { + List args = new ArrayList<>(); + args.addAll(Arrays.asList("codesign", + "-s", signingIdentity, // sign with this key + "--prefix", identifierPrefix, + // use the identifier as a prefix + "-vvvv")); + if (keyChain != null && !keyChain.isEmpty()) { + args.add("--keychain"); + args.add(keyChain); + } + args.add(path.toString()); + ProcessBuilder pb = new ProcessBuilder(args); + IOUtils.exec(pb); + + args = new ArrayList<>(); + args.addAll(Arrays.asList("codesign", + "-s", signingIdentity, // sign with this key + "--prefix", identifierPrefix, + // use the identifier as a prefix + "-vvvv")); + if (keyChain != null && !keyChain.isEmpty()) { + args.add("--keychain"); + args.add(keyChain); + } + args.add(path.toString() + + "/Contents/_CodeSignature/CodeResources"); + pb = new ProcessBuilder(args); + IOUtils.exec(pb); + } catch (IOException e) { + toThrow.set(e); + } + }; + + Path javaPath = appLocation.resolve("Contents/runtime"); + if (Files.isDirectory(javaPath)) { + signIdentifiedByPList.accept(javaPath); + + ioe = toThrow.get(); + if (ioe != null) { + throw ioe; + } + } + Path frameworkPath = appLocation.resolve("Contents/Frameworks"); + if (Files.isDirectory(frameworkPath)) { + Files.list(frameworkPath) + .forEach(signIdentifiedByPList); + + ioe = toThrow.get(); + if (ioe != null) { + throw ioe; + } + } + + // sign the app itself + List args = new ArrayList<>(); + args.addAll(Arrays.asList("codesign", + "-s", signingIdentity, // sign with this key + "-vvvv")); // super verbose output + if (entitlementsFile != null) { + args.add("--entitlements"); + args.add(entitlementsFile); // entitlements + } + if (keyChain != null && !keyChain.isEmpty()) { + args.add("--keychain"); + args.add(keyChain); + } + args.add(appLocation.toString()); + + ProcessBuilder pb = + new ProcessBuilder(args.toArray(new String[args.size()])); + IOUtils.exec(pb); + } + + private static boolean isFileSigned(Path file) { + ProcessBuilder pb = + new ProcessBuilder("codesign", "--verify", file.toString()); + + try { + IOUtils.exec(pb); + } catch (IOException ex) { + return false; + } + + return true; + } + + private static String extractBundleIdentifier(Map params) { + if (PREDEFINED_APP_IMAGE.fetchFrom(params) == null) { + return null; + } + + try { + File infoPList = new File(PREDEFINED_APP_IMAGE.fetchFrom(params) + + File.separator + "Contents" + + File.separator + "Info.plist"); + + DocumentBuilderFactory dbf + = DocumentBuilderFactory.newDefaultInstance(); + dbf.setFeature("http://apache.org/xml/features/" + + "nonvalidating/load-external-dtd", false); + DocumentBuilder b = dbf.newDocumentBuilder(); + org.w3c.dom.Document doc = b.parse(new FileInputStream( + infoPList.getAbsolutePath())); + + XPath xPath = XPathFactory.newInstance().newXPath(); + // Query for the value of element preceding + // element with value equal to CFBundleIdentifier + String v = (String) xPath.evaluate( + "//string[preceding-sibling::key = \"CFBundleIdentifier\"][1]", + doc, XPathConstants.STRING); + + if (v != null && !v.isEmpty()) { + return v; + } + } catch (Exception ex) { + Log.verbose(ex); + } + + return null; + } + +} diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppStoreBundler.java b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppStoreBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppStoreBundler.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.*; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; +import static jdk.incubator.jpackage.internal.MacAppBundler.*; +import static jdk.incubator.jpackage.internal.OverridableResource.createResource; + +public class MacAppStoreBundler extends MacBaseInstallerBundler { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MacResources"); + + private static final String TEMPLATE_BUNDLE_ICON_HIDPI = "java.icns"; + private final static String DEFAULT_ENTITLEMENTS = + "MacAppStore.entitlements"; + private final static String DEFAULT_INHERIT_ENTITLEMENTS = + "MacAppStore_Inherit.entitlements"; + + public static final BundlerParamInfo MAC_APP_STORE_APP_SIGNING_KEY = + new StandardBundlerParam<>( + "mac.signing-key-app", + String.class, + params -> { + String result = MacBaseInstallerBundler.findKey( + "3rd Party Mac Developer Application: " + + SIGNING_KEY_USER.fetchFrom(params), + SIGNING_KEYCHAIN.fetchFrom(params), + VERBOSE.fetchFrom(params)); + if (result != null) { + MacCertificate certificate = new MacCertificate(result); + + if (!certificate.isValid()) { + Log.error(MessageFormat.format( + I18N.getString("error.certificate.expired"), + result)); + } + } + + return result; + }, + (s, p) -> s); + + public static final BundlerParamInfo MAC_APP_STORE_PKG_SIGNING_KEY = + new StandardBundlerParam<>( + "mac.signing-key-pkg", + String.class, + params -> { + String result = MacBaseInstallerBundler.findKey( + "3rd Party Mac Developer Installer: " + + SIGNING_KEY_USER.fetchFrom(params), + SIGNING_KEYCHAIN.fetchFrom(params), + VERBOSE.fetchFrom(params)); + + if (result != null) { + MacCertificate certificate = new MacCertificate(result); + + if (!certificate.isValid()) { + Log.error(MessageFormat.format( + I18N.getString("error.certificate.expired"), + result)); + } + } + + return result; + }, + (s, p) -> s); + + public static final StandardBundlerParam MAC_APP_STORE_ENTITLEMENTS = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAC_APP_STORE_ENTITLEMENTS.getId(), + File.class, + params -> null, + (s, p) -> new File(s)); + + public static final BundlerParamInfo INSTALLER_SUFFIX = + new StandardBundlerParam<> ( + "mac.app-store.installerName.suffix", + String.class, + params -> "-MacAppStore", + (s, p) -> s); + + public File bundle(Map params, + File outdir) throws PackagerException { + Log.verbose(MessageFormat.format(I18N.getString( + "message.building-bundle"), APP_NAME.fetchFrom(params))); + + IOUtils.writableOutputDir(outdir.toPath()); + + // first, load in some overrides + // icns needs @2 versions, so load in the @2 default + params.put(DEFAULT_ICNS_ICON.getID(), TEMPLATE_BUNDLE_ICON_HIDPI); + + // now we create the app + File appImageDir = APP_IMAGE_TEMP_ROOT.fetchFrom(params); + try { + appImageDir.mkdirs(); + + try { + MacAppImageBuilder.addNewKeychain(params); + } catch (InterruptedException e) { + Log.error(e.getMessage()); + } + // first, make sure we don't use the local signing key + params.put(DEVELOPER_ID_APP_SIGNING_KEY.getID(), null); + File appLocation = prepareAppBundle(params); + + prepareEntitlements(params); + + String signingIdentity = + MAC_APP_STORE_APP_SIGNING_KEY.fetchFrom(params); + String identifierPrefix = + BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params); + String entitlementsFile = + getConfig_Entitlements(params).toString(); + String inheritEntitlements = + getConfig_Inherit_Entitlements(params).toString(); + + MacAppImageBuilder.signAppBundle(params, appLocation.toPath(), + signingIdentity, identifierPrefix, + entitlementsFile, inheritEntitlements); + MacAppImageBuilder.restoreKeychainList(params); + + ProcessBuilder pb; + + // create the final pkg file + File finalPKG = new File(outdir, INSTALLER_NAME.fetchFrom(params) + + INSTALLER_SUFFIX.fetchFrom(params) + + ".pkg"); + outdir.mkdirs(); + + String installIdentify = + MAC_APP_STORE_PKG_SIGNING_KEY.fetchFrom(params); + + List buildOptions = new ArrayList<>(); + buildOptions.add("productbuild"); + buildOptions.add("--component"); + buildOptions.add(appLocation.toString()); + buildOptions.add("/Applications"); + buildOptions.add("--sign"); + buildOptions.add(installIdentify); + buildOptions.add("--product"); + buildOptions.add(appLocation + "/Contents/Info.plist"); + String keychainName = SIGNING_KEYCHAIN.fetchFrom(params); + if (keychainName != null && !keychainName.isEmpty()) { + buildOptions.add("--keychain"); + buildOptions.add(keychainName); + } + buildOptions.add(finalPKG.getAbsolutePath()); + + pb = new ProcessBuilder(buildOptions); + + IOUtils.exec(pb); + return finalPKG; + } catch (PackagerException pe) { + throw pe; + } catch (Exception ex) { + Log.verbose(ex); + throw new PackagerException(ex); + } + } + + private File getConfig_Entitlements(Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + ".entitlements"); + } + + private File getConfig_Inherit_Entitlements( + Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "_Inherit.entitlements"); + } + + private void prepareEntitlements(Map params) + throws IOException { + createResource(DEFAULT_ENTITLEMENTS, params) + .setCategory( + I18N.getString("resource.mac-app-store-entitlements")) + .setExternal(MAC_APP_STORE_ENTITLEMENTS.fetchFrom(params)) + .saveToFile(getConfig_Entitlements(params)); + + createResource(DEFAULT_INHERIT_ENTITLEMENTS, params) + .setCategory(I18N.getString( + "resource.mac-app-store-inherit-entitlements")) + .saveToFile(getConfig_Entitlements(params)); + } + + /////////////////////////////////////////////////////////////////////// + // Implement Bundler + /////////////////////////////////////////////////////////////////////// + + @Override + public String getName() { + return I18N.getString("store.bundler.name"); + } + + @Override + public String getID() { + return "mac.appStore"; + } + + @Override + public boolean validate(Map params) + throws ConfigException { + try { + Objects.requireNonNull(params); + + // hdiutil is always available so there's no need to test for + // availability. + // run basic validation to ensure requirements are met + + // we are not interested in return code, only possible exception + validateAppImageAndBundeler(params); + + // reject explicitly set to not sign + if (!Optional.ofNullable(MacAppImageBuilder. + SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { + throw new ConfigException( + I18N.getString("error.must-sign-app-store"), + I18N.getString("error.must-sign-app-store.advice")); + } + + // make sure we have settings for signatures + if (MAC_APP_STORE_APP_SIGNING_KEY.fetchFrom(params) == null) { + throw new ConfigException( + I18N.getString("error.no-app-signing-key"), + I18N.getString("error.no-app-signing-key.advice")); + } + if (MAC_APP_STORE_PKG_SIGNING_KEY.fetchFrom(params) == null) { + throw new ConfigException( + I18N.getString("error.no-pkg-signing-key"), + I18N.getString("error.no-pkg-signing-key.advice")); + } + + // things we could check... + // check the icons, make sure it has hidpi icons + // check the category, + // make sure it fits in the list apple has provided + // validate bundle identifier is reverse dns + // check for \a+\.\a+\.. + + return true; + } catch (RuntimeException re) { + if (re.getCause() instanceof ConfigException) { + throw (ConfigException) re.getCause(); + } else { + throw new ConfigException(re); + } + } + } + + @Override + public File execute(Map params, + File outputParentDir) throws PackagerException { + return bundle(params, outputParentDir); + } + + @Override + public boolean supported(boolean runtimeInstaller) { + // return (!runtimeInstaller && + // Platform.getPlatform() == Platform.MAC); + return false; // mac-app-store not yet supported + } + + @Override + public boolean isDefault() { + return false; + } + +} diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacBaseInstallerBundler.java b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacBaseInstallerBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacBaseInstallerBundler.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + +public abstract class MacBaseInstallerBundler extends AbstractBundler { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MacResources"); + + // This could be generalized more to be for any type of Image Bundler + public static final BundlerParamInfo APP_BUNDLER = + new StandardBundlerParam<>( + "mac.app.bundler", + MacAppBundler.class, + params -> new MacAppBundler(), + (s, p) -> null); + + public final BundlerParamInfo APP_IMAGE_TEMP_ROOT = + new StandardBundlerParam<>( + "mac.app.imageRoot", + File.class, + params -> { + File imageDir = IMAGES_ROOT.fetchFrom(params); + if (!imageDir.exists()) imageDir.mkdirs(); + try { + return Files.createTempDirectory( + imageDir.toPath(), "image-").toFile(); + } catch (IOException e) { + return new File(imageDir, getID()+ ".image"); + } + }, + (s, p) -> new File(s)); + + public static final BundlerParamInfo SIGNING_KEY_USER = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAC_SIGNING_KEY_NAME.getId(), + String.class, + params -> "", + null); + + public static final BundlerParamInfo SIGNING_KEYCHAIN = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAC_SIGNING_KEYCHAIN.getId(), + String.class, + params -> "", + null); + + public static final BundlerParamInfo INSTALLER_NAME = + new StandardBundlerParam<> ( + "mac.installerName", + String.class, + params -> { + String nm = APP_NAME.fetchFrom(params); + if (nm == null) return null; + + String version = VERSION.fetchFrom(params); + if (version == null) { + return nm; + } else { + return nm + "-" + version; + } + }, + (s, p) -> s); + + protected void validateAppImageAndBundeler( + Map params) throws ConfigException { + if (PREDEFINED_APP_IMAGE.fetchFrom(params) != null) { + File applicationImage = PREDEFINED_APP_IMAGE.fetchFrom(params); + if (!applicationImage.exists()) { + throw new ConfigException( + MessageFormat.format(I18N.getString( + "message.app-image-dir-does-not-exist"), + PREDEFINED_APP_IMAGE.getID(), + applicationImage.toString()), + MessageFormat.format(I18N.getString( + "message.app-image-dir-does-not-exist.advice"), + PREDEFINED_APP_IMAGE.getID())); + } + if (APP_NAME.fetchFrom(params) == null) { + throw new ConfigException( + I18N.getString("message.app-image-requires-app-name"), + I18N.getString( + "message.app-image-requires-app-name.advice")); + } + } else { + APP_BUNDLER.fetchFrom(params).validate(params); + } + } + + protected File prepareAppBundle(Map params) + throws PackagerException { + File predefinedImage = + StandardBundlerParam.getPredefinedAppImage(params); + if (predefinedImage != null) { + return predefinedImage; + } + File appImageRoot = APP_IMAGE_TEMP_ROOT.fetchFrom(params); + + return APP_BUNDLER.fetchFrom(params).doBundle( + params, appImageRoot, true); + } + + @Override + public String getBundleType() { + return "INSTALLER"; + } + + public static String findKey(String key, String keychainName, + boolean verbose) { + if (Platform.getPlatform() != Platform.MAC) { + return null; + } + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos)) { + List searchOptions = new ArrayList<>(); + searchOptions.add("security"); + searchOptions.add("find-certificate"); + searchOptions.add("-c"); + searchOptions.add(key); + searchOptions.add("-a"); + if (keychainName != null && !keychainName.isEmpty()) { + searchOptions.add(keychainName); + } + + ProcessBuilder pb = new ProcessBuilder(searchOptions); + + IOUtils.exec(pb, false, ps); + Pattern p = Pattern.compile("\"alis\"=\"([^\"]+)\""); + Matcher m = p.matcher(baos.toString()); + if (!m.find()) { + Log.error("Did not find a key matching '" + key + "'"); + return null; + } + String matchedKey = m.group(1); + if (m.find()) { + Log.error("Found more than one key matching '" + key + "'"); + return null; + } + Log.verbose("Using key '" + matchedKey + "'"); + return matchedKey; + } catch (IOException ioe) { + Log.verbose(ioe); + return null; + } + } +} diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacCertificate.java b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacCertificate.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacCertificate.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.StandardCopyOption; +import java.nio.file.Files; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public final class MacCertificate { + private final String certificate; + + public MacCertificate(String certificate) { + this.certificate = certificate; + } + + public boolean isValid() { + return verifyCertificate(this.certificate); + } + + private static File findCertificate(String certificate) { + File result = null; + + List args = new ArrayList<>(); + args.add("security"); + args.add("find-certificate"); + args.add("-c"); + args.add(certificate); + args.add("-a"); + args.add("-p"); + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos)) { + ProcessBuilder security = new ProcessBuilder(args); + IOUtils.exec(security, false, ps); + + File output = File.createTempFile("tempfile", ".tmp"); + + Files.copy(new ByteArrayInputStream(baos.toByteArray()), + output.toPath(), StandardCopyOption.REPLACE_EXISTING); + + result = output; + } + catch (IOException ignored) {} + + return result; + } + + private static Date findCertificateDate(String filename) { + Date result = null; + + List args = new ArrayList<>(); + args.add("/usr/bin/openssl"); + args.add("x509"); + args.add("-noout"); + args.add("-enddate"); + args.add("-in"); + args.add(filename); + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos)) { + ProcessBuilder security = new ProcessBuilder(args); + IOUtils.exec(security, false, ps); + String output = baos.toString(); + output = output.substring(output.indexOf("=") + 1); + DateFormat df = new SimpleDateFormat( + "MMM dd kk:mm:ss yyyy z", Locale.ENGLISH); + result = df.parse(output); + } catch (IOException | ParseException ex) { + Log.verbose(ex); + } + + return result; + } + + private static boolean verifyCertificate(String certificate) { + boolean result = false; + + try { + File file = null; + Date certificateDate = null; + + try { + file = findCertificate(certificate); + + if (file != null) { + certificateDate = findCertificateDate( + file.getCanonicalPath()); + } + } + finally { + if (file != null) { + file.delete(); + } + } + + if (certificateDate != null) { + Calendar c = Calendar.getInstance(); + Date today = c.getTime(); + + if (certificateDate.after(today)) { + result = true; + } + } + } + catch (IOException ignored) {} + + return result; + } +} diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacDmgBundler.java b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacDmgBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacDmgBundler.java @@ -0,0 +1,480 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.*; +import java.nio.file.Files; +import java.text.MessageFormat; +import java.util.*; +import static jdk.incubator.jpackage.internal.OverridableResource.createResource; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + +public class MacDmgBundler extends MacBaseInstallerBundler { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MacResources"); + + static final String DEFAULT_BACKGROUND_IMAGE="background_dmg.png"; + static final String DEFAULT_DMG_SETUP_SCRIPT="DMGsetup.scpt"; + static final String TEMPLATE_BUNDLE_ICON = "java.icns"; + + static final String DEFAULT_LICENSE_PLIST="lic_template.plist"; + + public static final BundlerParamInfo INSTALLER_SUFFIX = + new StandardBundlerParam<> ( + "mac.dmg.installerName.suffix", + String.class, + params -> "", + (s, p) -> s); + + public File bundle(Map params, + File outdir) throws PackagerException { + Log.verbose(MessageFormat.format(I18N.getString("message.building-dmg"), + APP_NAME.fetchFrom(params))); + + IOUtils.writableOutputDir(outdir.toPath()); + + File appImageDir = APP_IMAGE_TEMP_ROOT.fetchFrom(params); + try { + appImageDir.mkdirs(); + + if (prepareAppBundle(params) != null && + prepareConfigFiles(params)) { + File configScript = getConfig_Script(params); + if (configScript.exists()) { + Log.verbose(MessageFormat.format( + I18N.getString("message.running-script"), + configScript.getAbsolutePath())); + IOUtils.run("bash", configScript); + } + + return buildDMG(params, outdir); + } + return null; + } catch (IOException ex) { + Log.verbose(ex); + throw new PackagerException(ex); + } + } + + private static final String hdiutil = "/usr/bin/hdiutil"; + + private void prepareDMGSetupScript(String volumeName, + Map params) throws IOException { + File dmgSetup = getConfig_VolumeScript(params); + Log.verbose(MessageFormat.format( + I18N.getString("message.preparing-dmg-setup"), + dmgSetup.getAbsolutePath())); + + //prepare config for exe + Map data = new HashMap<>(); + data.put("DEPLOY_ACTUAL_VOLUME_NAME", volumeName); + data.put("DEPLOY_APPLICATION_NAME", APP_NAME.fetchFrom(params)); + + data.put("DEPLOY_INSTALL_LOCATION", "(path to applications folder)"); + data.put("DEPLOY_INSTALL_NAME", "Applications"); + + createResource(DEFAULT_DMG_SETUP_SCRIPT, params) + .setCategory(I18N.getString("resource.dmg-setup-script")) + .setSubstitutionData(data) + .saveToFile(dmgSetup); + } + + private File getConfig_VolumeScript(Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-dmg-setup.scpt"); + } + + private File getConfig_VolumeBackground( + Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-background.png"); + } + + private File getConfig_VolumeIcon(Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-volume.icns"); + } + + private File getConfig_LicenseFile(Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-license.plist"); + } + + private void prepareLicense(Map params) { + try { + String licFileStr = LICENSE_FILE.fetchFrom(params); + if (licFileStr == null) { + return; + } + + File licFile = new File(licFileStr); + byte[] licenseContentOriginal = + Files.readAllBytes(licFile.toPath()); + String licenseInBase64 = + Base64.getEncoder().encodeToString(licenseContentOriginal); + + Map data = new HashMap<>(); + data.put("APPLICATION_LICENSE_TEXT", licenseInBase64); + + createResource(DEFAULT_LICENSE_PLIST, params) + .setCategory(I18N.getString("resource.license-setup")) + .setSubstitutionData(data) + .saveToFile(getConfig_LicenseFile(params)); + + } catch (IOException ex) { + Log.verbose(ex); + } + } + + private boolean prepareConfigFiles(Map params) + throws IOException { + + createResource(DEFAULT_BACKGROUND_IMAGE, params) + .setCategory(I18N.getString("resource.dmg-background")) + .saveToFile(getConfig_VolumeBackground(params)); + + createResource(TEMPLATE_BUNDLE_ICON, params) + .setCategory(I18N.getString("resource.volume-icon")) + .setExternal(MacAppBundler.ICON_ICNS.fetchFrom(params)) + .saveToFile(getConfig_VolumeIcon(params)); + + createResource(null, params) + .setCategory(I18N.getString("resource.post-install-script")) + .saveToFile(getConfig_Script(params)); + + prepareLicense(params); + + // In theory we need to extract name from results of attach command + // However, this will be a problem for customization as name will + // possibly change every time and developer will not be able to fix it + // As we are using tmp dir chance we get "different" name are low => + // Use fixed name we used for bundle + prepareDMGSetupScript(APP_NAME.fetchFrom(params), params); + + return true; + } + + // name of post-image script + private File getConfig_Script(Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-post-image.sh"); + } + + // Location of SetFile utility may be different depending on MacOS version + // We look for several known places and if none of them work will + // try ot find it + private String findSetFileUtility() { + String typicalPaths[] = {"/Developer/Tools/SetFile", + "/usr/bin/SetFile", "/Developer/usr/bin/SetFile"}; + + String setFilePath = null; + for (String path: typicalPaths) { + File f = new File(path); + if (f.exists() && f.canExecute()) { + setFilePath = path; + break; + } + } + + // Validate SetFile, if Xcode is not installed it will run, but exit with error + // code + if (setFilePath != null) { + try { + ProcessBuilder pb = new ProcessBuilder(setFilePath, "-h"); + Process p = pb.start(); + int code = p.waitFor(); + if (code == 0) { + return setFilePath; + } + } catch (Exception ignored) {} + + // No need for generic find attempt. We found it, but it does not work. + // Probably due to missing xcode. + return null; + } + + // generic find attempt + try { + ProcessBuilder pb = new ProcessBuilder("xcrun", "-find", "SetFile"); + Process p = pb.start(); + InputStreamReader isr = new InputStreamReader(p.getInputStream()); + BufferedReader br = new BufferedReader(isr); + String lineRead = br.readLine(); + if (lineRead != null) { + File f = new File(lineRead); + if (f.exists() && f.canExecute()) { + return f.getAbsolutePath(); + } + } + } catch (IOException ignored) {} + + return null; + } + + private File buildDMG( + Map params, File outdir) + throws IOException { + File imagesRoot = IMAGES_ROOT.fetchFrom(params); + if (!imagesRoot.exists()) imagesRoot.mkdirs(); + + File protoDMG = new File(imagesRoot, + APP_NAME.fetchFrom(params) +"-tmp.dmg"); + File finalDMG = new File(outdir, INSTALLER_NAME.fetchFrom(params) + + INSTALLER_SUFFIX.fetchFrom(params) + ".dmg"); + + File srcFolder = APP_IMAGE_TEMP_ROOT.fetchFrom(params); + File predefinedImage = + StandardBundlerParam.getPredefinedAppImage(params); + if (predefinedImage != null) { + srcFolder = predefinedImage; + } + + Log.verbose(MessageFormat.format(I18N.getString( + "message.creating-dmg-file"), finalDMG.getAbsolutePath())); + + protoDMG.delete(); + if (finalDMG.exists() && !finalDMG.delete()) { + throw new IOException(MessageFormat.format(I18N.getString( + "message.dmg-cannot-be-overwritten"), + finalDMG.getAbsolutePath())); + } + + protoDMG.getParentFile().mkdirs(); + finalDMG.getParentFile().mkdirs(); + + String hdiUtilVerbosityFlag = VERBOSE.fetchFrom(params) ? + "-verbose" : "-quiet"; + + // create temp image + ProcessBuilder pb = new ProcessBuilder( + hdiutil, + "create", + hdiUtilVerbosityFlag, + "-srcfolder", srcFolder.getAbsolutePath(), + "-volname", APP_NAME.fetchFrom(params), + "-ov", protoDMG.getAbsolutePath(), + "-fs", "HFS+", + "-format", "UDRW"); + IOUtils.exec(pb); + + // mount temp image + pb = new ProcessBuilder( + hdiutil, + "attach", + protoDMG.getAbsolutePath(), + hdiUtilVerbosityFlag, + "-mountroot", imagesRoot.getAbsolutePath()); + IOUtils.exec(pb); + + File mountedRoot = new File(imagesRoot.getAbsolutePath(), + APP_NAME.fetchFrom(params)); + + try { + // volume icon + File volumeIconFile = new File(mountedRoot, ".VolumeIcon.icns"); + IOUtils.copyFile(getConfig_VolumeIcon(params), + volumeIconFile); + + // background image + File bgdir = new File(mountedRoot, ".background"); + bgdir.mkdirs(); + IOUtils.copyFile(getConfig_VolumeBackground(params), + new File(bgdir, "background.png")); + + // Indicate that we want a custom icon + // NB: attributes of the root directory are ignored + // when creating the volume + // Therefore we have to do this after we mount image + String setFileUtility = findSetFileUtility(); + if (setFileUtility != null) { + //can not find utility => keep going without icon + try { + volumeIconFile.setWritable(true); + // The "creator" attribute on a file is a legacy attribute + // but it seems Finder excepts these bytes to be + // "icnC" for the volume icon + // (might not work on Mac 10.13 with old XCode) + pb = new ProcessBuilder( + setFileUtility, + "-c", "icnC", + volumeIconFile.getAbsolutePath()); + IOUtils.exec(pb); + volumeIconFile.setReadOnly(); + + pb = new ProcessBuilder( + setFileUtility, + "-a", "C", + mountedRoot.getAbsolutePath()); + IOUtils.exec(pb); + } catch (IOException ex) { + Log.error(ex.getMessage()); + Log.verbose("Cannot enable custom icon using SetFile utility"); + } + } else { + Log.verbose(I18N.getString("message.setfile.dmg")); + } + + // We will not consider setting background image and creating link to + // /Application folder in DMG as critical error, since it can fail in + // headless enviroment. + try { + pb = new ProcessBuilder("osascript", + getConfig_VolumeScript(params).getAbsolutePath()); + IOUtils.exec(pb); + } catch (IOException ex) { + Log.verbose(ex); + } + } finally { + // Detach the temporary image + pb = new ProcessBuilder( + hdiutil, + "detach", + "-force", + hdiUtilVerbosityFlag, + mountedRoot.getAbsolutePath()); + IOUtils.exec(pb); + } + + // Compress it to a new image + pb = new ProcessBuilder( + hdiutil, + "convert", + protoDMG.getAbsolutePath(), + hdiUtilVerbosityFlag, + "-format", "UDZO", + "-o", finalDMG.getAbsolutePath()); + IOUtils.exec(pb); + + //add license if needed + if (getConfig_LicenseFile(params).exists()) { + //hdiutil unflatten your_image_file.dmg + pb = new ProcessBuilder( + hdiutil, + "unflatten", + finalDMG.getAbsolutePath() + ); + IOUtils.exec(pb); + + //add license + pb = new ProcessBuilder( + hdiutil, + "udifrez", + finalDMG.getAbsolutePath(), + "-xml", + getConfig_LicenseFile(params).getAbsolutePath() + ); + IOUtils.exec(pb); + + //hdiutil flatten your_image_file.dmg + pb = new ProcessBuilder( + hdiutil, + "flatten", + finalDMG.getAbsolutePath() + ); + IOUtils.exec(pb); + + } + + //Delete the temporary image + protoDMG.delete(); + + Log.verbose(MessageFormat.format(I18N.getString( + "message.output-to-location"), + APP_NAME.fetchFrom(params), finalDMG.getAbsolutePath())); + + return finalDMG; + } + + + ////////////////////////////////////////////////////////////////////////// + // Implement Bundler + ////////////////////////////////////////////////////////////////////////// + + @Override + public String getName() { + return I18N.getString("dmg.bundler.name"); + } + + @Override + public String getID() { + return "dmg"; + } + + @Override + public boolean validate(Map params) + throws ConfigException { + try { + Objects.requireNonNull(params); + + //run basic validation to ensure requirements are met + //we are not interested in return code, only possible exception + validateAppImageAndBundeler(params); + + return true; + } catch (RuntimeException re) { + if (re.getCause() instanceof ConfigException) { + throw (ConfigException) re.getCause(); + } else { + throw new ConfigException(re); + } + } + } + + @Override + public File execute(Map params, + File outputParentDir) throws PackagerException { + return bundle(params, outputParentDir); + } + + @Override + public boolean supported(boolean runtimeInstaller) { + return isSupported(); + } + + public final static String[] required = + {"/usr/bin/hdiutil", "/usr/bin/osascript"}; + public static boolean isSupported() { + try { + for (String s : required) { + File f = new File(s); + if (!f.exists() || !f.canExecute()) { + return false; + } + } + return true; + } catch (Exception e) { + return false; + } + } + + @Override + public boolean isDefault() { + return true; + } + +} diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacPkgBundler.java b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacPkgBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacPkgBundler.java @@ -0,0 +1,555 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.*; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; +import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEYCHAIN; +import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEY_USER; +import static jdk.incubator.jpackage.internal.MacAppImageBuilder.MAC_CF_BUNDLE_IDENTIFIER; +import static jdk.incubator.jpackage.internal.OverridableResource.createResource; + +public class MacPkgBundler extends MacBaseInstallerBundler { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MacResources"); + + private static final String DEFAULT_BACKGROUND_IMAGE = "background_pkg.png"; + + private static final String TEMPLATE_PREINSTALL_SCRIPT = + "preinstall.template"; + private static final String TEMPLATE_POSTINSTALL_SCRIPT = + "postinstall.template"; + + private static final BundlerParamInfo PACKAGES_ROOT = + new StandardBundlerParam<>( + "mac.pkg.packagesRoot", + File.class, + params -> { + File packagesRoot = + new File(TEMP_ROOT.fetchFrom(params), "packages"); + packagesRoot.mkdirs(); + return packagesRoot; + }, + (s, p) -> new File(s)); + + + protected final BundlerParamInfo SCRIPTS_DIR = + new StandardBundlerParam<>( + "mac.pkg.scriptsDir", + File.class, + params -> { + File scriptsDir = + new File(CONFIG_ROOT.fetchFrom(params), "scripts"); + scriptsDir.mkdirs(); + return scriptsDir; + }, + (s, p) -> new File(s)); + + public static final + BundlerParamInfo DEVELOPER_ID_INSTALLER_SIGNING_KEY = + new StandardBundlerParam<>( + "mac.signing-key-developer-id-installer", + String.class, + params -> { + String result = MacBaseInstallerBundler.findKey( + "Developer ID Installer: " + + SIGNING_KEY_USER.fetchFrom(params), + SIGNING_KEYCHAIN.fetchFrom(params), + VERBOSE.fetchFrom(params)); + if (result != null) { + MacCertificate certificate = new MacCertificate(result); + + if (!certificate.isValid()) { + Log.error(MessageFormat.format( + I18N.getString("error.certificate.expired"), + result)); + } + } + + return result; + }, + (s, p) -> s); + + public static final BundlerParamInfo MAC_INSTALL_DIR = + new StandardBundlerParam<>( + "mac-install-dir", + String.class, + params -> { + String dir = INSTALL_DIR.fetchFrom(params); + return (dir != null) ? dir : "/Applications"; + }, + (s, p) -> s + ); + + public static final BundlerParamInfo INSTALLER_SUFFIX = + new StandardBundlerParam<> ( + "mac.pkg.installerName.suffix", + String.class, + params -> "", + (s, p) -> s); + + public File bundle(Map params, + File outdir) throws PackagerException { + Log.verbose(MessageFormat.format(I18N.getString("message.building-pkg"), + APP_NAME.fetchFrom(params))); + + IOUtils.writableOutputDir(outdir.toPath()); + + try { + File appImageDir = prepareAppBundle(params); + + if (appImageDir != null && prepareConfigFiles(params)) { + + File configScript = getConfig_Script(params); + if (configScript.exists()) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.running-script"), + configScript.getAbsolutePath())); + IOUtils.run("bash", configScript); + } + + return createPKG(params, outdir, appImageDir); + } + return null; + } catch (IOException ex) { + Log.verbose(ex); + throw new PackagerException(ex); + } + } + + private File getPackages_AppPackage(Map params) { + return new File(PACKAGES_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-app.pkg"); + } + + private File getConfig_DistributionXMLFile( + Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), "distribution.dist"); + } + + private File getConfig_BackgroundImage(Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-background.png"); + } + + private File getConfig_BackgroundImageDarkAqua(Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-background-darkAqua.png"); + } + + private File getScripts_PreinstallFile(Map params) { + return new File(SCRIPTS_DIR.fetchFrom(params), "preinstall"); + } + + private File getScripts_PostinstallFile( + Map params) { + return new File(SCRIPTS_DIR.fetchFrom(params), "postinstall"); + } + + private String getAppIdentifier(Map params) { + return MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params); + } + + private void preparePackageScripts(Map params) + throws IOException { + Log.verbose(I18N.getString("message.preparing-scripts")); + + Map data = new HashMap<>(); + + Path appLocation = Path.of(MAC_INSTALL_DIR.fetchFrom(params), + APP_NAME.fetchFrom(params) + ".app", "Contents", "app"); + + data.put("INSTALL_LOCATION", MAC_INSTALL_DIR.fetchFrom(params)); + data.put("APP_LOCATION", appLocation.toString()); + + createResource(TEMPLATE_PREINSTALL_SCRIPT, params) + .setCategory(I18N.getString("resource.pkg-preinstall-script")) + .setSubstitutionData(data) + .saveToFile(getScripts_PreinstallFile(params)); + getScripts_PreinstallFile(params).setExecutable(true, false); + + createResource(TEMPLATE_POSTINSTALL_SCRIPT, params) + .setCategory(I18N.getString("resource.pkg-postinstall-script")) + .setSubstitutionData(data) + .saveToFile(getScripts_PostinstallFile(params)); + getScripts_PostinstallFile(params).setExecutable(true, false); + } + + private static String URLEncoding(String pkgName) throws URISyntaxException { + URI uri = new URI(null, null, pkgName, null); + return uri.toASCIIString(); + } + + private void prepareDistributionXMLFile(Map params) + throws IOException { + File f = getConfig_DistributionXMLFile(params); + + Log.verbose(MessageFormat.format(I18N.getString( + "message.preparing-distribution-dist"), f.getAbsolutePath())); + + IOUtils.createXml(f.toPath(), xml -> { + xml.writeStartElement("installer-gui-script"); + xml.writeAttribute("minSpecVersion", "1"); + + xml.writeStartElement("title"); + xml.writeCharacters(APP_NAME.fetchFrom(params)); + xml.writeEndElement(); + + xml.writeStartElement("background"); + xml.writeAttribute("file", getConfig_BackgroundImage(params).getName()); + xml.writeAttribute("mime-type", "image/png"); + xml.writeAttribute("alignment", "bottomleft"); + xml.writeAttribute("scaling", "none"); + xml.writeEndElement(); + + xml.writeStartElement("background-darkAqua"); + xml.writeAttribute("file", getConfig_BackgroundImageDarkAqua(params).getName()); + xml.writeAttribute("mime-type", "image/png"); + xml.writeAttribute("alignment", "bottomleft"); + xml.writeAttribute("scaling", "none"); + xml.writeEndElement(); + + String licFileStr = LICENSE_FILE.fetchFrom(params); + if (licFileStr != null) { + File licFile = new File(licFileStr); + xml.writeStartElement("license"); + xml.writeAttribute("file", licFile.getAbsolutePath()); + xml.writeAttribute("mime-type", "text/rtf"); + xml.writeEndElement(); + } + + /* + * Note that the content of the distribution file + * below is generated by productbuild --synthesize + */ + String appId = getAppIdentifier(params); + + xml.writeStartElement("pkg-ref"); + xml.writeAttribute("id", appId); + xml.writeEndElement(); // + xml.writeStartElement("options"); + xml.writeAttribute("customize", "never"); + xml.writeAttribute("require-scripts", "false"); + xml.writeEndElement(); // + xml.writeStartElement("choices-outline"); + xml.writeStartElement("line"); + xml.writeAttribute("choice", "default"); + xml.writeStartElement("line"); + xml.writeAttribute("choice", appId); + xml.writeEndElement(); // + xml.writeEndElement(); // + xml.writeEndElement(); // + xml.writeStartElement("choice"); + xml.writeAttribute("id", "default"); + xml.writeEndElement(); // + xml.writeStartElement("choice"); + xml.writeAttribute("id", appId); + xml.writeAttribute("visible", "false"); + xml.writeStartElement("pkg-ref"); + xml.writeAttribute("id", appId); + xml.writeEndElement(); // + xml.writeEndElement(); // + xml.writeStartElement("pkg-ref"); + xml.writeAttribute("id", appId); + xml.writeAttribute("version", VERSION.fetchFrom(params)); + xml.writeAttribute("onConclusion", "none"); + try { + xml.writeCharacters(URLEncoding( + getPackages_AppPackage(params).getName())); + } catch (URISyntaxException ex) { + throw new IOException(ex); + } + xml.writeEndElement(); // + + xml.writeEndElement(); // + }); + } + + private boolean prepareConfigFiles(Map params) + throws IOException { + + createResource(DEFAULT_BACKGROUND_IMAGE, params) + .setCategory(I18N.getString("resource.pkg-background-image")) + .saveToFile(getConfig_BackgroundImage(params)); + + createResource(DEFAULT_BACKGROUND_IMAGE, params) + .setCategory(I18N.getString("resource.pkg-background-image")) + .saveToFile(getConfig_BackgroundImageDarkAqua(params)); + + prepareDistributionXMLFile(params); + + createResource(null, params) + .setCategory(I18N.getString("resource.post-install-script")) + .saveToFile(getConfig_Script(params)); + + return true; + } + + // name of post-image script + private File getConfig_Script(Map params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-post-image.sh"); + } + + private void patchCPLFile(File cpl) throws IOException { + String cplData = Files.readString(cpl.toPath()); + String[] lines = cplData.split("\n"); + try (PrintWriter out = new PrintWriter(Files.newBufferedWriter( + cpl.toPath()))) { + int skip = 0; + // Used to skip Java.runtime bundle, since + // pkgbuild with --root will find two bundles app and Java runtime. + // We cannot generate component proprty list when using + // --component argument. + for (int i = 0; i < lines.length; i++) { + if (lines[i].trim().equals("BundleIsRelocatable")) { + out.println(lines[i]); + out.println(""); + i++; + } else if (lines[i].trim().equals("ChildBundles")) { + ++skip; + } else if ((skip > 0) && lines[i].trim().equals("")) { + --skip; + } else { + if (skip == 0) { + out.println(lines[i]); + } + } + } + } + } + + // pkgbuild includes all components from "--root" and subfolders, + // so if we have app image in folder which contains other images, then they + // will be included as well. It does have "--filter" option which use regex + // to exclude files/folder, but it will overwrite default one which excludes + // based on doc "any .svn or CVS directories, and any .DS_Store files". + // So easy aproach will be to copy user provided app-image into temp folder + // if root path contains other files. + private String getRoot(Map params, + File appLocation) throws IOException { + String root = appLocation.getParent() == null ? + "." : appLocation.getParent(); + File rootDir = new File(root); + File[] list = rootDir.listFiles(); + if (list != null) { // Should not happend + // We should only have app image and/or .DS_Store + if (list.length == 1) { + return root; + } else if (list.length == 2) { + // Check case with app image and .DS_Store + if (list[0].toString().toLowerCase().endsWith(".ds_store") || + list[1].toString().toLowerCase().endsWith(".ds_store")) { + return root; // Only app image and .DS_Store + } + } + } + + // Copy to new root + Path newRoot = Files.createTempDirectory( + TEMP_ROOT.fetchFrom(params).toPath(), + "root-"); + + IOUtils.copyRecursive(appLocation.toPath(), + newRoot.resolve(appLocation.getName())); + + return newRoot.toString(); + } + + private File createPKG(Map params, + File outdir, File appLocation) { + // generic find attempt + try { + File appPKG = getPackages_AppPackage(params); + + String root = getRoot(params, appLocation); + + // Generate default CPL file + File cpl = new File(CONFIG_ROOT.fetchFrom(params).getAbsolutePath() + + File.separator + "cpl.plist"); + ProcessBuilder pb = new ProcessBuilder("pkgbuild", + "--root", + root, + "--install-location", + MAC_INSTALL_DIR.fetchFrom(params), + "--analyze", + cpl.getAbsolutePath()); + + IOUtils.exec(pb); + + patchCPLFile(cpl); + + preparePackageScripts(params); + + // build application package + pb = new ProcessBuilder("pkgbuild", + "--root", + root, + "--install-location", + MAC_INSTALL_DIR.fetchFrom(params), + "--component-plist", + cpl.getAbsolutePath(), + "--scripts", + SCRIPTS_DIR.fetchFrom(params).getAbsolutePath(), + appPKG.getAbsolutePath()); + IOUtils.exec(pb); + + // build final package + File finalPKG = new File(outdir, INSTALLER_NAME.fetchFrom(params) + + INSTALLER_SUFFIX.fetchFrom(params) + + ".pkg"); + outdir.mkdirs(); + + List commandLine = new ArrayList<>(); + commandLine.add("productbuild"); + + commandLine.add("--resources"); + commandLine.add(CONFIG_ROOT.fetchFrom(params).getAbsolutePath()); + + // maybe sign + if (Optional.ofNullable(MacAppImageBuilder. + SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { + if (Platform.getMajorVersion() > 10 || + (Platform.getMajorVersion() == 10 && + Platform.getMinorVersion() >= 12)) { + // we need this for OS X 10.12+ + Log.verbose(I18N.getString("message.signing.pkg")); + } + + String signingIdentity = + DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(params); + if (signingIdentity != null) { + commandLine.add("--sign"); + commandLine.add(signingIdentity); + } + + String keychainName = SIGNING_KEYCHAIN.fetchFrom(params); + if (keychainName != null && !keychainName.isEmpty()) { + commandLine.add("--keychain"); + commandLine.add(keychainName); + } + } + + commandLine.add("--distribution"); + commandLine.add( + getConfig_DistributionXMLFile(params).getAbsolutePath()); + commandLine.add("--package-path"); + commandLine.add(PACKAGES_ROOT.fetchFrom(params).getAbsolutePath()); + + commandLine.add(finalPKG.getAbsolutePath()); + + pb = new ProcessBuilder(commandLine); + IOUtils.exec(pb); + + return finalPKG; + } catch (Exception ignored) { + Log.verbose(ignored); + return null; + } + } + + ////////////////////////////////////////////////////////////////////////// + // Implement Bundler + ////////////////////////////////////////////////////////////////////////// + + @Override + public String getName() { + return I18N.getString("pkg.bundler.name"); + } + + @Override + public String getID() { + return "pkg"; + } + + @Override + public boolean validate(Map params) + throws ConfigException { + try { + Objects.requireNonNull(params); + + // run basic validation to ensure requirements are met + // we are not interested in return code, only possible exception + validateAppImageAndBundeler(params); + + if (MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) == null) { + throw new ConfigException( + I18N.getString("message.app-image-requires-identifier"), + I18N.getString( + "message.app-image-requires-identifier.advice")); + } + + // reject explicitly set sign to true and no valid signature key + if (Optional.ofNullable(MacAppImageBuilder. + SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) { + String signingIdentity = + DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(params); + if (signingIdentity == null) { + throw new ConfigException( + I18N.getString("error.explicit-sign-no-cert"), + I18N.getString( + "error.explicit-sign-no-cert.advice")); + } + } + + // hdiutil is always available so there's no need + // to test for availability. + + return true; + } catch (RuntimeException re) { + if (re.getCause() instanceof ConfigException) { + throw (ConfigException) re.getCause(); + } else { + throw new ConfigException(re); + } + } + } + + @Override + public File execute(Map params, + File outputParentDir) throws PackagerException { + return bundle(params, outputParentDir); + } + + @Override + public boolean supported(boolean runtimeInstaller) { + return true; + } + + @Override + public boolean isDefault() { + return false; + } + +} diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/DMGsetup.scpt b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/DMGsetup.scpt new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/DMGsetup.scpt @@ -0,0 +1,38 @@ +tell application "Finder" + tell disk "DEPLOY_ACTUAL_VOLUME_NAME" + open + set current view of container window to icon view + set toolbar visible of container window to false + set statusbar visible of container window to false + + -- size of window should match size of background + set the bounds of container window to {400, 100, 917, 380} + + set theViewOptions to the icon view options of container window + set arrangement of theViewOptions to not arranged + set icon size of theViewOptions to 128 + set background picture of theViewOptions to file ".background:background.png" + + -- Create alias for install location + make new alias file at container window to DEPLOY_INSTALL_LOCATION with properties {name:"DEPLOY_INSTALL_NAME"} + + set allTheFiles to the name of every item of container window + repeat with theFile in allTheFiles + set theFilePath to POSIX Path of theFile + if theFilePath is "/DEPLOY_APPLICATION_NAME.app" + -- Position application location + set position of item theFile of container window to {120, 130} + else if theFilePath is "/DEPLOY_INSTALL_NAME" + -- Position install location + set position of item theFile of container window to {390, 130} + else + -- Move all other files far enough to be not visible if user has "show hidden files" option set + set position of item theFile of container window to {1000, 130} + end + end repeat + + update without registering applications + delay 5 + close + end tell +end tell diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/Info-lite.plist.template b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/Info-lite.plist.template new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/Info-lite.plist.template @@ -0,0 +1,37 @@ + + + + + LSMinimumSystemVersion + 10.9 + CFBundleDevelopmentRegion + English + CFBundleAllowMixedLocalizations + + CFBundleExecutable + DEPLOY_LAUNCHER_NAME + CFBundleIconFile + DEPLOY_ICON_FILE + CFBundleIdentifier + DEPLOY_BUNDLE_IDENTIFIER + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + DEPLOY_BUNDLE_NAME + CFBundlePackageType + APPL + CFBundleShortVersionString + DEPLOY_BUNDLE_SHORT_VERSION + CFBundleSignature + ???? + + LSApplicationCategoryType + Unknown + CFBundleVersion + DEPLOY_BUNDLE_CFBUNDLE_VERSION + NSHumanReadableCopyright + DEPLOY_BUNDLE_COPYRIGHTDEPLOY_FILE_ASSOCIATIONS + NSHighResolutionCapable + true + + diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacAppStore.entitlements b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacAppStore.entitlements new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacAppStore.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacAppStore_Inherit.entitlements b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacAppStore_Inherit.entitlements new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacAppStore_Inherit.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.inherit + + + diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources.properties b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources.properties @@ -0,0 +1,89 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +app.bundler.name=Mac Application Image +store.bundler.name=Mac App Store Ready Bundler +dmg.bundler.name=Mac DMG Package +pkg.bundler.name=Mac PKG Package + +error.invalid-cfbundle-version=Invalid CFBundleVersion: [{0}] +error.invalid-cfbundle-version.advice=Set a compatible 'appVersion' or set a 'mac.CFBundleVersion'. Valid versions are one to three integers separated by dots. +error.explicit-sign-no-cert=Signature explicitly requested but no signing certificate specified +error.explicit-sign-no-cert.advice=Either specify a valid cert in 'mac.signing-key-developer-id-app' or unset 'signBundle' or set 'signBundle' to false. +error.must-sign-app-store=Mac App Store apps must be signed, and signing has been disabled by bundler configuration +error.must-sign-app-store.advice=Either unset 'signBundle' or set 'signBundle' to true +error.no-app-signing-key=No Mac App Store App Signing Key +error.no-app-signing-key.advice=Install your app signing keys into your Mac Keychain using XCode. +error.no-pkg-signing-key=No Mac App Store Installer Signing Key +error.no-pkg-signing-key.advice=Install your app signing keys into your Mac Keychain using XCode. +error.certificate.expired=Error: Certificate expired {0} +error.no.xcode.signing=Xcode with command line developer tools is required for signing +error.no.xcode.signing.advice=Install Xcode with command line developer tools. + +resource.bundle-config-file=Bundle config file +resource.app-info-plist=Application Info.plist +resource.runtime-info-plist=Java Runtime Info.plist +resource.mac-app-store-entitlements=Mac App Store Entitlements +resource.mac-app-store-inherit-entitlements=Mac App Store Inherit Entitlements +resource.dmg-setup-script=DMG setup script +resource.license-setup=License setup +resource.dmg-background=dmg background +resource.volume-icon=volume icon +resource.post-install-script=script to run after application image is populated +resource.pkg-preinstall-script=PKG preinstall script +resource.pkg-postinstall-script=PKG postinstall script +resource.pkg-background-image=pkg background image + + +message.bundle-name-too-long-warning={0} is set to ''{1}'', which is longer than 16 characters. For a better Mac experience consider shortening it. +message.null-classpath=Null app resources? +message.preparing-info-plist=Preparing Info.plist: {0}. +message.icon-not-icns= The specified icon "{0}" is not an ICNS file and will not be used. The default icon will be used in it's place. +message.version-string-too-many-components=Version sting may have between 1 and 3 numbers: 1, 1.2, 1.2.3. +message.version-string-first-number-not-zero=The first number in a CFBundleVersion cannot be zero or negative. +message.version-string-no-negative-numbers=Negative numbers are not allowed in version strings. +message.version-string-numbers-only=Version strings can consist of only numbers and up to two dots. +message.creating-association-with-null-extension=Creating association with null extension. +message.ignoring.symlink=Warning: codesign is skipping the symlink {0}. +message.keychain.error=Error: unable to get keychain list. +message.building-bundle=Building Mac App Store Package for {0}. +message.app-image-dir-does-not-exist=Specified application image directory {0}: {1} does not exists. +message.app-image-dir-does-not-exist.advice=Confirm that the value for {0} exists. +message.app-image-requires-app-name=When using an external app image you must specify the app name. +message.app-image-requires-app-name.advice=Set the app name via the -name CLI flag, the fx:application/@name ANT attribute, or via the 'appName' bundler argument. +message.app-image-requires-identifier=Unable to extract identifier from app image. +message.app-image-requires-identifier.advice=Use "--verbose" for extended error message or specify it via "--mac-package-identifier". +message.building-dmg=Building DMG package for {0}. +message.running-script=Running shell script on application image [{0}]. +message.preparing-dmg-setup=Preparing dmg setup: {0}. +message.creating-dmg-file=Creating DMG file: {0}. +message.dmg-cannot-be-overwritten=Dmg file exists ({0} and can not be removed. +message.output-to-location=Result DMG installer for {0}: {1}. +message.building-pkg=Building PKG package for {0}. +message.preparing-scripts=Preparing package scripts. +message.preparing-distribution-dist=Preparing distribution.dist: {0}. +message.signing.pkg=Warning: For signing PKG, you might need to set "Always Trust" for your certificate using "Keychain Access" tool. +message.setfile.dmg=Setting custom icon on DMG file skipped because 'SetFile' utility was not found. Installing Xcode with Command Line Tools should resolve this issue. diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources_ja.properties b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources_ja.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources_ja.properties @@ -0,0 +1,89 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +app.bundler.name=Mac Application Image +store.bundler.name=Mac App Store Ready Bundler +dmg.bundler.name=Mac DMG Package +pkg.bundler.name=Mac PKG Package + +error.invalid-cfbundle-version=Invalid CFBundleVersion: [{0}] +error.invalid-cfbundle-version.advice=Set a compatible 'appVersion' or set a 'mac.CFBundleVersion'. Valid versions are one to three integers separated by dots. +error.explicit-sign-no-cert=Signature explicitly requested but no signing certificate specified +error.explicit-sign-no-cert.advice=Either specify a valid cert in 'mac.signing-key-developer-id-app' or unset 'signBundle' or set 'signBundle' to false. +error.must-sign-app-store=Mac App Store apps must be signed, and signing has been disabled by bundler configuration +error.must-sign-app-store.advice=Either unset 'signBundle' or set 'signBundle' to true +error.no-app-signing-key=No Mac App Store App Signing Key +error.no-app-signing-key.advice=Install your app signing keys into your Mac Keychain using XCode. +error.no-pkg-signing-key=No Mac App Store Installer Signing Key +error.no-pkg-signing-key.advice=Install your app signing keys into your Mac Keychain using XCode. +error.certificate.expired=Error: Certificate expired {0} +error.no.xcode.signing=Xcode with command line developer tools is required for signing +error.no.xcode.signing.advice=Install Xcode with command line developer tools. + +resource.bundle-config-file=Bundle config file +resource.app-info-plist=Application Info.plist +resource.runtime-info-plist=Java Runtime Info.plist +resource.mac-app-store-entitlements=Mac App Store Entitlements +resource.mac-app-store-inherit-entitlements=Mac App Store Inherit Entitlements +resource.dmg-setup-script=DMG setup script +resource.license-setup=License setup +resource.dmg-background=dmg background +resource.volume-icon=volume icon +resource.post-install-script=script to run after application image is populated +resource.pkg-preinstall-script=PKG preinstall script +resource.pkg-postinstall-script=PKG postinstall script +resource.pkg-background-image=pkg background image + + +message.bundle-name-too-long-warning={0} is set to ''{1}'', which is longer than 16 characters. For a better Mac experience consider shortening it. +message.null-classpath=Null app resources? +message.preparing-info-plist=Preparing Info.plist: {0}. +message.icon-not-icns= The specified icon "{0}" is not an ICNS file and will not be used. The default icon will be used in it's place. +message.version-string-too-many-components=Version sting may have between 1 and 3 numbers: 1, 1.2, 1.2.3. +message.version-string-first-number-not-zero=The first number in a CFBundleVersion cannot be zero or negative. +message.version-string-no-negative-numbers=Negative numbers are not allowed in version strings. +message.version-string-numbers-only=Version strings can consist of only numbers and up to two dots. +message.creating-association-with-null-extension=Creating association with null extension. +message.ignoring.symlink=Warning: codesign is skipping the symlink {0}. +message.keychain.error=Error: unable to get keychain list. +message.building-bundle=Building Mac App Store Package for {0}. +message.app-image-dir-does-not-exist=Specified application image directory {0}: {1} does not exists. +message.app-image-dir-does-not-exist.advice=Confirm that the value for {0} exists. +message.app-image-requires-app-name=When using an external app image you must specify the app name. +message.app-image-requires-app-name.advice=Set the app name via the -name CLI flag, the fx:application/@name ANT attribute, or via the 'appName' bundler argument. +message.app-image-requires-identifier=Unable to extract identifier from app image. +message.app-image-requires-identifier.advice=Use "--verbose" for extended error message or specify it via "--mac-package-identifier". +message.building-dmg=Building DMG package for {0}. +message.running-script=Running shell script on application image [{0}]. +message.preparing-dmg-setup=Preparing dmg setup: {0}. +message.creating-dmg-file=Creating DMG file: {0}. +message.dmg-cannot-be-overwritten=Dmg file exists ({0} and can not be removed. +message.output-to-location=Result DMG installer for {0}: {1}. +message.building-pkg=Building PKG package for {0}. +message.preparing-scripts=Preparing package scripts. +message.preparing-distribution-dist=Preparing distribution.dist: {0}. +message.signing.pkg=Warning: For signing PKG, you might need to set "Always Trust" for your certificate using "Keychain Access" tool. +message.setfile.dmg=Setting custom icon on DMG file skipped because 'SetFile' utility was not found. Installing Xcode with Command Line Tools should resolve this issue. diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources_zh_CN.properties b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources_zh_CN.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources_zh_CN.properties @@ -0,0 +1,89 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +app.bundler.name=Mac Application Image +store.bundler.name=Mac App Store Ready Bundler +dmg.bundler.name=Mac DMG Package +pkg.bundler.name=Mac PKG Package + +error.invalid-cfbundle-version=Invalid CFBundleVersion: [{0}] +error.invalid-cfbundle-version.advice=Set a compatible 'appVersion' or set a 'mac.CFBundleVersion'. Valid versions are one to three integers separated by dots. +error.explicit-sign-no-cert=Signature explicitly requested but no signing certificate specified +error.explicit-sign-no-cert.advice=Either specify a valid cert in 'mac.signing-key-developer-id-app' or unset 'signBundle' or set 'signBundle' to false. +error.must-sign-app-store=Mac App Store apps must be signed, and signing has been disabled by bundler configuration +error.must-sign-app-store.advice=Either unset 'signBundle' or set 'signBundle' to true +error.no-app-signing-key=No Mac App Store App Signing Key +error.no-app-signing-key.advice=Install your app signing keys into your Mac Keychain using XCode. +error.no-pkg-signing-key=No Mac App Store Installer Signing Key +error.no-pkg-signing-key.advice=Install your app signing keys into your Mac Keychain using XCode. +error.certificate.expired=Error: Certificate expired {0} +error.no.xcode.signing=Xcode with command line developer tools is required for signing +error.no.xcode.signing.advice=Install Xcode with command line developer tools. + +resource.bundle-config-file=Bundle config file +resource.app-info-plist=Application Info.plist +resource.runtime-info-plist=Java Runtime Info.plist +resource.mac-app-store-entitlements=Mac App Store Entitlements +resource.mac-app-store-inherit-entitlements=Mac App Store Inherit Entitlements +resource.dmg-setup-script=DMG setup script +resource.license-setup=License setup +resource.dmg-background=dmg background +resource.volume-icon=volume icon +resource.post-install-script=script to run after application image is populated +resource.pkg-preinstall-script=PKG preinstall script +resource.pkg-postinstall-script=PKG postinstall script +resource.pkg-background-image=pkg background image + + +message.bundle-name-too-long-warning={0} is set to ''{1}'', which is longer than 16 characters. For a better Mac experience consider shortening it. +message.null-classpath=Null app resources? +message.preparing-info-plist=Preparing Info.plist: {0}. +message.icon-not-icns= The specified icon "{0}" is not an ICNS file and will not be used. The default icon will be used in it's place. +message.version-string-too-many-components=Version sting may have between 1 and 3 numbers: 1, 1.2, 1.2.3. +message.version-string-first-number-not-zero=The first number in a CFBundleVersion cannot be zero or negative. +message.version-string-no-negative-numbers=Negative numbers are not allowed in version strings. +message.version-string-numbers-only=Version strings can consist of only numbers and up to two dots. +message.creating-association-with-null-extension=Creating association with null extension. +message.ignoring.symlink=Warning: codesign is skipping the symlink {0}. +message.keychain.error=Error: unable to get keychain list. +message.building-bundle=Building Mac App Store Package for {0}. +message.app-image-dir-does-not-exist=Specified application image directory {0}: {1} does not exists. +message.app-image-dir-does-not-exist.advice=Confirm that the value for {0} exists. +message.app-image-requires-app-name=When using an external app image you must specify the app name. +message.app-image-requires-app-name.advice=Set the app name via the -name CLI flag, the fx:application/@name ANT attribute, or via the 'appName' bundler argument. +message.app-image-requires-identifier=Unable to extract identifier from app image. +message.app-image-requires-identifier.advice=Use "--verbose" for extended error message or specify it via "--mac-package-identifier". +message.building-dmg=Building DMG package for {0}. +message.running-script=Running shell script on application image [{0}]. +message.preparing-dmg-setup=Preparing dmg setup: {0}. +message.creating-dmg-file=Creating DMG file: {0}. +message.dmg-cannot-be-overwritten=Dmg file exists ({0} and can not be removed. +message.output-to-location=Result DMG installer for {0}: {1}. +message.building-pkg=Building PKG package for {0}. +message.preparing-scripts=Preparing package scripts. +message.preparing-distribution-dist=Preparing distribution.dist: {0}. +message.signing.pkg=Warning: For signing PKG, you might need to set "Always Trust" for your certificate using "Keychain Access" tool. +message.setfile.dmg=Setting custom icon on DMG file skipped because 'SetFile' utility was not found. Installing Xcode with Command Line Tools should resolve this issue. diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/Runtime-Info.plist.template b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/Runtime-Info.plist.template new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/Runtime-Info.plist.template @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + libjli.dylib + CFBundleIdentifier + CF_BUNDLE_IDENTIFIER + CFBundleInfoDictionaryVersion + 7.0 + CFBundleName + CF_BUNDLE_NAME + CFBundlePackageType + BNDL + CFBundleShortVersionString + CF_BUNDLE_SHORT_VERSION_STRING + CFBundleSignature + ???? + CFBundleVersion + CF_BUNDLE_VERSION + + diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/background_dmg.png b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/background_dmg.png new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2cfefac72e0ab4d13ab3d6475d653d90d40f01fa GIT binary patch literal 3407 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 U?VGJj%YSiP$sVMuP5~$W355rqyZ`_I diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/background_pkg.png b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/background_pkg.png new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ae9f28bfe1decb53aeea6bf3321702af74eb6f0a GIT binary patch literal 17374 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
OQGVjAGT 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 qiTW(CKPcq?#^1I6FGAmMaC)djcqty`um1TD9P% diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/java.icns b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/java.icns new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..fc60195cd0e919d1e184c2ce198ed59b3d487efa GIT binary patch literal 85755 zc%1CL2UJwc)~LH0n$#c}77K|IL?lU$l2MW%AOa#8L873Nh3+OtNfHczq5?`3QB)8l zh>8k`iUJZPNRXV7e%0)~|LZx&z5jFGyW`*YjzD#(h5NtE&1dL@j0otcV(p`lmgn-UOO|> zQyTE` z8*-lcegu;Kpc@|*k{)!4M%BFUoxVEFne2gQ})%^i>vuB=PzO%XskURe^GnpazI0UZ;}Tt zklN#3myFuUZUUiALIGEkNcev_GwPpbhJ&nrniv(ev;@V^6WbH#$MVy}WMS81G?M>o zVy!~E+2!8;G_kUir{4dcI57-n|Gs~i7^km2gxLKv6=WxAXYaMAMb5H67Kz>h4B05x99lyKc3@%@ASVu z@$q(cKRxFDV=Dpr*XOQ(dWOcw|MB_fF29)QAA6bPT25VNE4nV{+U4c!Et2KSHCHxK zHH}v`P>s(LHc+i@aT};-wejnu<3e2BvFoVX$8l@uWo260ld)^4#}DFG$h=q}hCWhbJuovrHP*8m3~pw|QPXH-}#631SJuy1S0kK?~&h^GmqcT!)Q%uAAqWcZBnpOHn$3};qX=lss z53~!PLZx2BO)`}jOipUNCnP#_4-M8RHxV{?cMpe8qAtc^CurY^*-v!I5C~f-iW&#? zpJNFqy|WP$n7F8M#EJ(J8PD>>6HqG~;IFiRh;ejO_*juNaEY`+Nh|?XXBjbu2@f50 zhIpZ1*1aR3s?9iJlqxiMKoRD527#0j2^xd zF^KZ<3LhZtKQ4DMYJenF=vd?c%FF#qKg!+p$`_QY@s%$mM$|8}d(khcsVEL*+LIij zpaPNdH1ieuDTtJ%>`juT%TGX%Jo!CR)=@8A8$oUxr2Z)pv^pXNCt##}Q zF;bSvJeeVgb1-IkJ5o?J>{tjzd-|tHSt6O@qa;QO_aul4VRjG|Wp`s1QMPm-QWzug ziy%^F7Z>jmBc+v{7%6CoOz@u~g@Ic|k&^-1{ZpjmM9omwanMM_XXEVzXfV3i8H!>M zDf?$~E=NtX>S9>DuiT4wR$~T(o1S*ibw5%#eK`X^#WZpMk`xC7yM!kOo={?jnA4|n zyzx`SNSS0B5{mw!uh}en#L?xX&VH3_8R0-%5DlqGxCy2b{fP;+_k?I+b?t-u`5Flu zJnTc^6R4y(>^NyGD%VWBq@R>k^Lx7HxMLl+mT{)r58lXi|iis zi~su~g-cQH*O4L?5`E6=5o=3?Km9DNMQr_fEAg|n{MYUOP4muY=d<(qtIvP8zy7=Z8A14~{Y{AY zgCvQWX(VB~F@Z4C5J#AM7DHI5iy|yNjU?b}NuG^uju1#T$DZM9!E+5X_Kg6I;cLM6 zC(tJNzKMSVzCWG>KcK7zWi==tg7P6KAAs@!DDQ*vJ}4_eSqaK}pu7jla!{6ovJ{l1 zpezAp2`KM^@-8TgKv@LJ0#Fu!@-`@MgYqUQZ-O$vnt(djSPH&?hrEXbk_^NUJmrC> z+y?}74F`fWmVocz`C1hL)h&QUAVW}b4OCpYPe9Gc;1Q^`7(9TA%asJwh9MRqWkC>A zP?msOQ1Hrf`D%2Mi3?}I9m~f z{oNf<2kKMH31pKJ2m+&V5Gjzi0`;k&KDi8-(+6rP@Zhcl9Xg9ZC1^-0BR~P5o;m^| zWkwJ?P@M#-<4XzXDs}{+i`*99{izVtf%>?TE%eY{1YwTe7I!M#!pDK?nBpz6b!}jl zEP7j>qrYGa9|IbqiZ`J^pdJ&uE$>yZiH`!+5qCGy4~gpIw#9kIZg1ivKt2A>26{pi zs84`MSrH`g(gwQ8{NU-2u^V_iXvTu(lL*2_mH?q?5QJ|gZ3DWGgHV`TF&lU+5V%mZ z4&4IgXp$gO7(t@b)=@n)K!9N;dL4fO2m}|dqXz{Lgz+MT-i06{OKIznCLV$kKzlG~ z4=h|mA0f^Iw4>P&5P~qILJ%1ct4LizEmGnj$Q=my0fBS3m(h+y{b>*>1A^$LEko>}IVE}-e-1SJ z-dchl5?3P~f+#@wGIa?O#zACUmaeBmn(<3`Um$em<`Q~F8mxWB_7Xb*8BrWWPQ%Q` z#!Q9BE#c1qA+MW@5T3Z+kq{Z05`&>6i`jNH_fYa8YK{S}L8tx#ELr^TlkR6W0uqaLYI5@1x3SR{Z(ia`;I~&o zKXDdn1{T>OAV?oOi+2WH9k0(o6T?Ox zkRj+z0eZ7wr}6fnw{6}OREkA7C?JRtZ18c=dww4DCt4pJ`8qThfFLZuIyHg_jmJ+x zlHm{~9Wyf-JBsBazxJmp;py5RBBvgq-xG1*Ij>^ejsCdRf90$>ZdO(G4`1UeGZ-9am zK!IiM1fRigPC$^u-zKL1Wgh+gCI~`--S*?|_puY0WC#LB75YG^6)P5kOz;zUOQ7g@ z&N$SFMR>qcfQ@_U+r;GbKRFa=%J^+Mz@_+@af+uHNNK2{dH_34I#v>JJA52}94I=L zGe&B^eeO}xBgngPq9)vT`ahO+2i zsF)Gqk!=kV*h%^c;s8drN6_Eb12llZ1Z(dZIYN34Vr@+x!5;+*jMBdDY6>-w+|9y{ z0pWrmyKoQ%6T^}jgMo;=)ib>A5d>Rg6u)#Auy%;_^+gIm?`p8 zzv2ynF8zx`Rq!dpet3wd`p&{H?)W`_#D|U!# zduqJ};PAr{c5PK#g2-frD0SMcA?Nf#ydF?&6;4rLePU;x`s2|9Ni1{cS86oKL@IF`T?yv_K6*8-h2lD?2oK!QNLH^mS{usxbUJ^>CwGH6fc7rX}OrIy%B+6XpER4=I&g1CeBDjo#k_o&A9 zc%SrQN`G9l6Fh54N&N!h;C9Z1hE7X>m^Py?0AH9w`$PaBm#N^IOzfCpa?oB zU_X;yH;sFb`;2!}1%eFO>5`ixfq@^aWNN1#rz+F@Y3^Ajh)0@Nz&*2HOQT5{2wB43J3+?;=e`5XBM#=|cpO z1tKy)L>l`MVoe0M{7w8vQcDCmTS7pOAPDFC%#V0!&@P4jfG$5FvzsgqqM|t2`r*Sf zkYd~~C6IzdpepkNUJA$@i1eX(L{zx@Nko|VL z-2oC{SzI$a@ghJ%7}p8;fNPgQfeM_Xm}i}x?l|H;9r{ho4u}Z@v3=m2nqVsnQF zI9b(j9prJU`<1~3Qob$cp4oxl2juqRI;hTrO-SL?-qDc?Bts%Phz>OZV*Utn28iti zVuJW~(pLqyp5fXd91u#4XeZw1K|sg@LHsh?@q$1|Ag&!c`FktyMJ{LsK^g{H1wbnw zXjKNSd7xDUL0*AYur(4vD<5dx16qYaYeRTDx)rSJd@2FcQxJK^-p(sL=lNnXfm9Yj z@__IjAj}ijPPz|*_<^Qs1QEDMARSCK5{TXov=@m4QbDj_K$HiFas$z`bR1WKXg;{q z69^=88LAf%61lXsSOR($tXwOQHjw84 z^1FdNxWyf#2qcr995{qT%%?W7o#fdWKCol=H%AgkJrU#-5Z(=h*@3Vh2#N>-$<}27 zAgq+umeP)DJPu@en<5CL3Lw&eEIW`zev@?wC!oF@0*2)-XS9>0fI~r`4Zj`O?2SMY z0g^C~j7AUxJOPynav_qvF>P7xWOu-MAoK}GAU4Tox8vd8Bq>WkqCG(%d@%x&>?fP9 zw9^y-rTaetx&!+m;CD%uOYIbG;HZ-FIL1Ys$d#OS>NK!?f+g!t2*TS8#8`-88SPMP zB!W8LoY#(uVvj%|zstY1FlDx*g1EuBWiIEpqtf~D2uk-YXk#L_WwoOlqWo|rx7*2U zPKh8wo~>7YYhjpd$20s?(t(ohr;-+ww4mGt%3YwO0VT~(B?gq3UzAkPi*N1F3p^G0 zP6>sA@1b}~@STE$F1Tf+ounn04o?wDSfYp`ERe+z=E&j*Gw1}uG<1pZ1-kb8Pa@&} zVr2FG8Cla|{hyHa2|En`jI5veurQ?mGqOtIVd&M*$f}QpNts~y=ugP%g@sYnl%VIU zpO7^V3lou*0fxNAkNINJ<04 z!oNaR1K>WgWI$^$eB~F!nnTnNh?N$GWq(1eBZ9zd+r$b(<-a0U2QUFNAXYjUKJ*J> zeLz&dO|0-=5UUhedO)loA{_seSaCoD8If2isHiB=G25Qz{3~MRhT&b{>ZGD$35Q8o z=!wJ%76JEX#A*ZlL?l+`SQy1Z6S3{k{nLL&tc_r~w~3Vj2UF}(&^0#Hl;*?1DCTWq z1-`!ir^Kqi9p@+t7A#Do=p7#$5$f;bVrwM9PIMK1+mm{KLagOjm=$0W1<)^aHuch_ zw8W?|KQE^frrP|(&O~Gdo1yJbkX0Q73a9~*Bo}k(YHr@uOGyzSzOJ^%3^av_d+E15 z=KH6}3KNkPtdUGq>eZWf?%ukZ78`cn-TwFyEoBfTOt|eO5&SD;1tXw`Ag}`kLgFsv z-nn0OH!mXr>*wiUX{41!AD)`~|YYTx0++VE82V7b^B`kAoP@x_^SK`oB2= ztnJy*n17jvhV|53MLB zATA;-xQ7cNb{1#@z&Z}j$zKAk1q`1i0xbw`ghvo7OfDqI&&$oh2F_5iHh{D$VE6nQ zX~BK{5FqU<0p<~Gp+%YQ<&o-llz zNVLU{QfzpblwBo&NVMzVM)@nEm4)G}M566_^`yCrNVIw|jQn&HjvN zFT$`qAle5oEd48@1zz+a5-kkx{&S)gfng%i?g7QGi56ryb3g!OJZb+*v@om!VDkhF zU-(y|g<)^db{1qpe?qii2NVGbuq-ZrNwhFD_?wvRUlOenxL?Y*M4PU15hjsM-6mQP!aoyj z5=b40MC-yG4pVWfobD(YArXis z5^WK1mS7hk+DLFc{zSCJFw8X$h}IBDvRM;}_BK!|+y#hsKMec*Othjfe8A0=gE*1P zfM`>}@(C2LJAy?1XQGXXgi*(cL>tK#0Ym40BHBPsFm9=AK(uLlz(I8AABZ;c9JZK9 zw9dk?pvMl;?hx$`(e4oK|M5i2E2DY%km`Xw|2fghItB;&`FOb4S?Y=XXGAOLg2hMR zF8KSpJDoH?qWPZ@tyBO$CO#oH;)0*ItDVJBU5)>UXb%KnW0KM@U5v#A`MNn+ndoUM z@BI&nmd`5)AD@2Z>ZOG6;Ir-yCrtIVRAjjRBceU#`wJB!|31;~JM9zr$21iI&~M*4e}7d|*(}?@?u< zuYFKXLYV*GCRz?ND?4X5FCSlDA20V)fJBcRR#%b{6XN|(h!*HUjLfWT9b8=9++1Cp z>`s^)>1rL6JFs5>Y|4M1X!-siS{)-Z3u{|@M@M^GYfCdDJ#AG*X)z%_fZ6{B(K7ub z(aP%@9yL3Tv)W?;FjQAtRY_J{n4cSL=l`5&1vL-p85$kMn$Q^^(L1cEsw5+^UyyhA ze?+uY+eE9Tbx2nit4F7MNK5^oqO63--aVXPNdF$uvIC;!S5#3|*U$t&t9npLPFj5b zUOq0M;NK%!b|TRV%E~J$DJv@}D#%JphygAo4&dKHTDEPZJs>45BO@&(DK55Oh@Xdp zm=^xqNXsjX6{8T@FT9t34<~~CPnedHos)~3hl^u35ruc4b_Z&ApmqmpccAwFGt~a^ zp6Va(!0ivI{)>N>{J;0JR2?GnPp{x*{7>2b>*x#|gV}Hl~ zj{P0`JN9?%@7Uk5zhi&L{*L_}`#bh`?C;p$vA<(~$NrA}9s4`>ckJ)j-?6`Af5-lg z{T=%|_IK>>*x#|gV}Hl~j{P0`JN9?%@7Uk5zhi&L{*L_}`#bh`?C;qBKimH5-(&v2 z-hX@Y_t^ih_y5BF9{d0G{^Q^M{^#HQ{_EfV{_o%Z{xbxLTzK?9_8RK)qYMB1{`b!3 zZ~FZ4|EZ2OK5}RmhWMZOfp+O0)-r)06h!=?BnO4HZ{aBjBAq?0scEdMsmWvP>wW69 zhcg5Tr6eBv;@xA$tQzZWeUnu+uOf+-CBE#K>4`_X*-M_2>!#iMrosN`%_rfTFVB0i zn|8@Q==406yG-Iuvr3A+N5PdGputYlw0v(Gbzj$1o#g}nO_g)qYOZO^_&aAx%S@k~ zd|sVW7N#)fe{iNlMy=)e<>4${#IA4tq)cwg#oVk6C39Y?Hub%WY6Z80k36;(BN2NZ zdFW%KE$_2FIg0K{!rAlYMFph%IT{|vRW5MNoyMjYDV5Uk^y#e4?(0|Yol_p-6cdcG zun))>7HO2?;HHx|oe*-!h`-rYB|Ie1^h4^vng`7S2fm8WNOXez@#~K~^kJp$k93>F`JRrhB7$KobG6T{1rh_q)#kG= z2j6U1dnv~{X*$Ex7hCPN8d+rg)b&leV6~@j;M;?DqOQ6>mWxvfhZ*|sbZVrG@u^)p zb+*_xCU`QjqP(mNT>wVYZZY)Jyrd3KF7-xV#&QIdtp(KJsv&m1)+B{fJles7Y_7F%FX z>pUGef^GC^%Gy=jl`W%lx^0l*dj!Xob4-y_En__UQZ=2j60in+{N5>z{PFJ?e9v?~ z;D~Q?`0{yi;h=Q;{$bAW*N<{^Y?)_@gWt86g)OeIx^JXhJ-t6SfTF&U_I?)W+}3Ubuaa&t6pLmp&qI6^*@o>-|)p^230d08#R*^|c>9ELX^v;jKuIkg)`)M~li=dTwdDztfL8Y~S;q zw$jq;K-1MMmQJ4d;ARG&!yGQvmvXBq&hu4|JutYom4$IlU@1Gxlcgwo_jM3&d!X8d zpl%m~;+w=Jx>3}<3-bU5okJ0`^mANQu`8>Ja$l13>{Gq5>w~dv#u#-s5 z6)%}9bhA$1NeIJXl~X%zU;9!2=hHbRJnJU6ygbVO8a3eD_0g>xkD`@fGC{`tSSsFTX>6~LgfIR3FSza!&_lenkHILpk7d>jpLfgdx@!!`838^{6dD~d1?^e*#gvlEx_ni0j3n5Y_ zSFB3>N{=&Rn#Dn94F{bvsv`AQBs@`zNR-dL6QG`3J_~1>!hTZ(GFsOJvrkZxoRnY! zQ$&luC&@F$quo%(Ld=;wRuD(aS|vB=q1l^aNvyoY#!gBU@OR1(hA;w$^VHSA&=`O2 zmB$K2GV-_c@I;!#UKfKRHGHUu4eBIS5Lhk!_9}>3FL@y8wF!*XpkgefR=aL&jkh9J zsx3f5>3THBz;}VD=iAc8)I_1036geY2y@Fp-JQfFJ*^*E-8U(6!2H&OJfLAXoOy z@sl^Jq4OxABWDNdQJe1bP7X>fJ}+9^9&TA%8>`(X`B--1Pze^o3PW+Gy^qdPc-PL6 z8=(8Sp!xX)dujF()hm#z{w=Z-66}dtydrEj(z(r0#vBe3Y!V@#B$Pu|lLHJx)luhy zE@Uq>H#au{QaV3*hR~-{wv;xcNEDqP+?W~OTw7>bb|_)hZf?He2`1Z&-mg~BIT+0y zI;+RM@4LDSN+@;H<-2vUZC%c%QaQG%&CmmJRIgCb$6`yq)0W00bZ#g!Mp|yg2=Av` zmp{Gr<(Q|&a2#xLlDwWJ!Dno3osb*0{&M=DiE`szR>+Wymdn@IcXo8Q_b0L?k3oCd zbzL$t&y;&48D$nY5(;y4vs}BGogQM5(fope?+jt5TtA1Gwjg4Fw zYqXOHSPY4qxz4IaPjqY0*TXMIYDK!@36v!fsw^of z88b~58F?c+^t&2{oTR>NVf2|+R$yKL_Z>*cA}tl4{^4%ywBoh5BMsap<4Zv|BwxOI z)!<$CW3PUS8C`|X2nYZd6326v<^8kc7$cJgt=H6_2*#UIQo zP8roI)y(fPlpJ1P9!F9Qw=nf23Nra#7+8tF{=tpmhFRQ$AFIO`$Cnm6L%&fp2NqHr z)81BW5)tF>6@B07v|^70~bz-*wnD~JoG+fZ-3pkes4Ct|J>e@7%CDm+D--u&^*_nNjD$%*w)RT@qzr?iaE)^T4w@^oQca`yx|Xm4#if zn00+qEK|kVu_I5u8CO{)zP+Y2+(@UYm3#0)NFSX)xeM3=60)A%5sABH>3gW-<7zU- zW~jhWoGmw{9iwuChn4z*XH@Qc_j8>-ecIWVH!eP&sBLkwi>^(1fd*V@Upt=~!#pou zzJ2?=fU)Q)c|HSKsH~DMGczlr9OLJ5A3k4xVdi7ZAh;x4-f|S&**6W2MsVGx8&Tgf zCZqpav-17P3%=E5lpo-=!`oa)xG0wY1gF9b``=cdP3DHraMfDSD+ptl#Cw z+!d0%Xu`!?`WeLIyeZ5n)SkCu+$E4#Mxfg5gYcvdAInnV(R}h#)#GoAEh?+M(P}ViMwAyz_ ziq{N<-ZxXbA?BkOJ6w&O+h|U{N5U#*Wj5%N-q6s{Ug3Bs^SfnTXsuDGcbPDxXIJhz zsKKG&8}Z4;*kau0w79JU*tM06{$BmhZ{L?zU3syw`29=It;x#D$_jM{!Is>1HZ_P_ z&a1cn{XWwef^x}`y$TKF7?TT|ZQ^z58~vX<^KI)w#R!J)-o4xGKE!xV;S97e7tPIh zAvj}r^TDe&(?v%8$hI7%=*DQ;wMU2gZk_JD9TOYt^DU>h*Sc&a{5uV^RdmNDGotCZ2Hay4mE+GV>f$h3 zV5u2O%{6&5Gf($~4M3&VkML2uc4ug|)o5yHkZi3E(-o9?zb{Pb8hLST>?k!4w6@r3 z6icNl(!$U~!YTUf+Moq%<^F1x_6MFPGE2QXZzpzb418dNjzJ@hNz*-Z{hmEB(_1$7 zqlTFH_}H!S(KlD-W}lG)M+H|8k$?nAd9I1cW+uGf8e*qh+@*MpHrME?&}SQl8(Sve zGKz?f&Qt)4b-=q%*&x|slKzFXSMR59?!!Yv^juuu&nw&zQzLVkNZ0UljV!LhVvGWAf$#gQlu zwpbbnGWn#m=O4OQE-Uk)O=X_TAtR9ugjMtX)17C&y%Q*Vl?H|wx3WF`{M396Z?Ch2aue9|0?+P6q9j@(p zC~`Wk1RN=a;*vE;PmYk1L+&OfXNFdN9D;eq)}MrTxJLBZVTJi`8uZ+XhkX{ukAjW) zlp|;q`M$ac`~&_31_s8&#c5bruwgJ5V`F11NR0SoWLP}A?>_^pXIvX}{-w*BAu{aweT>JGbnNTvEHKJ9 z$~_3u=0lf+kHxgzRIkUpxDNui(^jN)kLnAZFlwu7No<{m`zt9vN7%XY40{BX30P!Z zx)gc!D#s-6F-svnKGMc$y3CN$emr(@_H9bwnk2Z2H8;mrB`3VU~+oMH)=8-jRq_vn;Nf$}mf<%RgMMmUHCwzw!{_UaDEs@&V?Ge%8 z_F0milOIbOTbKmZbU(R?v}X-##7eoiQKkm0&hl2g;mp2;$-DZg)*YthQWoldzMzaz zVf&$ZOnBi%sfoH6@05cC4e2cn6!=qUHkx+yX!~xOQP2AL_*yGp>${z?Zz(NqqxUx? z)Hlb&6<%KybL;j>OEZ6Ye4g*Y23UfT0i^h4L|E7oOGRyU^{0~hTtQ_HdwcsE)LzQk zEQ}<_4pRmsl3Uesfg3kJCr9~XwmKnAnFq9Aww z?K-QHD4mk1-WClb4dOu0NynX6WF8rLG|ldMUU5EsU~?^?mx9`*=Go^;m$}OLyKK$) zF!sdfVhn}S!j~}AtRiZO>@xhh({eCYjNCUjTXicYq`}uFykp$#d9KEFHLZZ^uhF-q zC8N}@38xgoEV#!h424{*ac3eqcrn=b}R}zJxiNl9HMdLd|ep_*{|gZX>xn z@kv!f49E?10GFM@EC)rFh^^(U2z9yOtPZW6WmbZq)7Mc)TR+-mC)N59Jju`=E-3k> zQ%qb8*5taHP3Y6cr@Dn|G?JV zJfm}kEm5b~vXXpbdAbp7VB!!vCSBa!1evL84zYG5pCU83Wp716jNUB(sN)uS5q zb#&MD^aBHdS!B7xs3g)h2|J7Dn;+1#5F4d!AgeN+&{t(EE$tiH0rGu?rqpuraTS{% zIX#{b0Jvq~6h&x!A8~-hRZP?-^)jP%ofl_cFyTT$dQM%I!r9xcD}@o3EXO1IGm48v zlvjGs$_LEmxmro=A|aQ^&d&aHFHowYwjH+*De91C-n#w4VRo{8EO~uJeZ2oMlFqBA zb{)z$!!kV8;1ypJ&b(6h$_Z;tuH&_ZK}adbUaPhmD~ZtLN*6|pdRp0ABu-FXicu=m z&@dJdel?;HV&c$V8@y8P-X)fqGot_f%aiD|y+)5iY_+r^T0UF_*||iL-L6M`m1d~b z*wdy3cxTIw2M#6XzTM)Ido+BDPFD+~(^8)}PQQRV9vwX?!Hbd5yR~+AB=}Zuh!C?K zv!sOHBc4THwS98bbq3@*f~5KM-AzzcvIFpO6+J#)4_c#7_-Plf-CO~*p$6|y|NdYQMmO3@c0!q{6D zgi_gl$guBf5Xi_D{8);cAz#!;yu;AoDw;4ZdAq4}EMT^VX21fRD}7F0B;E%$6lQ%N zeb!E%!zPfjJr8-|G@9$2u$26gt!dw-)$2z%J!r!szFAtf$D*THd=+l&;hR(26a0p? zwu3UfBZi)#b#QjDvHHR!tJ}vrBkwCiURF4^^gb>tFQ>1lsNl*x-`ds&Us9hSK?_NR zuHshx_lt=MuD=3jP)su0nyrCH?5#58y_9t%2^yzmS1v7|Qq4D0yPY1Mvj4RhPJ{M^ zr%qrtM?yk{Fl{AE{U@s0W4!5#Id>Uj1JAqVvp?&lJ1Ts%`K6u$^m*5SJZ)>}#+z?r ztEmfPZBH(g$-TRy8%z%}M@S)X2)#VhFWxO!RhDZ~l)kv8dN_zAEyy4Ddd~~YqtEw` zXBh@9T$kYHJ%y8d0?BsutZmZpz&asOD7I&tE^H7D+n~!`=V$esQf$)4A|34CSQTm; zlh|Lsegtk6$`ImsyiYW7TxTr}T!lo#agPr!y*IrvRzyvY-4v?0a4sdHZTIyKH9k9w>*RM7F}sx5Ts)JqIB=DMuG}4D z&!MA8blWToxUW}c`d3yJz?sM(i|KQfR%GEZD>7T1ZKNxrHfE3qp7C)sN*a#I#|bK5 z%|hhvo!p-gw0S^j;VJ{(e>5+rfn&h*6^{NHiSa95(^pWWLCkk4jWb4UgU*s!hXQqy zUHZC{BuT`t-qT)H&=G8*>+qq3vrE$s&`2No_&V*?T_eld?jo1fIQOp7z;j_{X3P$b zjtuPVZpWzsO7qIfvd+!T#Uv)O>|%A3W)E5U;(z$?VfqUX`bD72RO1|~KUVoF5}8*` zvDVg-MWLQG{rebL?aAy|^&V*>pA1I!y>1N+%%NzzEQ-=0pQUl3MN2lsh&#LsPr=y- z%zk9BsUJOoG-4}N#I29pUaxO@A-pQ`&|^T{?3PBMo_4S3Cp^e_p4q9z#4sv{tV(?E zubY{-3F&L-6#aZyS1qWvPmE40zk}!CK`u-riDopdHn_}F($gbiVkmEHf{YYfeP&BJ ziIMeQIme;b>!ycv;x2C;n2-orFgqk6Yx3d++V)EjiMcjUS}T@bfGx7&xmom@s*P3o z(e*KtbYxDTlx)^422-oqe0){+jq9fy0zy;(Yd_pIS6HI4H7+>9`EVvNF0KKjhF2dy z0DH>P(z5#eBr99gv&y!E?qiZM>fu;aQ}3g*0(=AqkkB6OhoPtA^YVL+?eRi+OR>*3 z*stXbzcvka{=VJH(h4ftL@`+K)#{hd-!=G*Gk*TJIC%u0l2L` zUuIHt&YfzBn0(F+X-9W;@YrF`OpY4focYYxEhw`d&A_^HuF#V6i~9$9^uwlgRzdziJ3ZMq`wyibgs6Wbk3V~FSH zei@LN?o)4Ph>PrfdY<}F635)jWve8KYy+<+sPC-)$L@Y8G%ZNde3UPyhLr$G_{(Bd zMhA@AbBWc$68gmo!KFN{m_YmVvB3H!oRkmLiqbKcHyG7c75o^|=PT6`{&4g4{KuO- zee(vb=gn`CDpBXZ9`^{oHoLGN!_ts_kGn!*uLuOCR^ZKIt>T_KHAGOYF0`qBdEE6y zdcY+o@OjZ`vpP$NS(1+XVADs@6Iwkt_Yj0H>76?n*5h`-m6c^5r6`4(`MiqDR*s9| z=x0a*`?{Ep&HNPY@?1trf5>cc9*z7n;Q@K|@ij(mhZO&}4#)Ct?h=@GE~adyYY1dv zTQn1@csU}NmrmMC>zrcd;LgtLU6a>*_tt3Z>0vU&PELHREwQR< zXyFc{0Z7rBDbA;&!nv_BJA;0di6fP~dz0Ip2vTFL8E3EAdA_DZMd<=D-Zn<)FI3DZnW-G zdZty8R@tiCM~2)nr&EJtS*bIYe)xb@}tVm5&iS=b2W)#BYm zAKVUvtjh(YHDf82gjJ0>hThD$EFwtwgV1mu7e>lfE0$-mnLHp612)} zYOT9{*z#D_9#wixTYY7Uvj-sga%s+iq(k;L#z`4{_T(w9>y~Ffay4G=TGGs7`-+lK zrn3AIuz#VyuBxrP*qmyTd{_Tbc_?5~!>Y8Y}~ zk@%&9Vl68E->p6Nv2?})Tm&&Z(N#9n>Hh@thExb_bp?}C>E(HylL7_U!`doW+~3QF zKISRUP~$p&gED}Rx~~ZbM_iMmd{u9^X0z(%@azP0WGoXirArm2e*WWFW)qoSx{+>3 z-hJ)*9?qKPRQem?i2-v>ye=yu$yPjzwsKvgN{t{DnSCSI|LL0P-47Nu0SBzA+(s%_ z9#b2VEi(bss1BIBasGlo;DqvXa}GmGNg3Ewy^}KLcO&z-!2qzha*ODZH#Y>}h7ifY zTo22ad&lf@pSmE|J$26aAgGM0k5u;!;BH}5Pq4f`HhEazfjWcE@|$M0Sa);(iAC?i zuJ}>3^&?-h;pDrnhGs}92Q5m=UW=r1x8etlVn`kLZ-%wZ%?U5&f(p2+hg z&iMQHrIo^L0Ab2nZ8~~MXLIO0(Y?EMgT>eWD+ODW6txPeL^7!aF1kVF6UIIRJNWwU zx<$HSSJmY)yxJ9yQzRBg&hUw3U<2dYZ5U>kZ3Fdd@)iTrEl3(R_8jIGpxD>^V4MnU z;w~DFm5xJ4R9hL8+Z^8)9<2^uDgCl0{&bb3?chdnSKqLwD}8%Q0=KHoIcgyja_#|Z z6lH}*mc6e+Kt^S0%ftA^1w7+`<$@WHIDBWBvNc%r?y`sl-x8kDj!dBb^6QTgwL=XFM{^&&Yw)U|^>EoGGOB8u>hoeg=VFFpEtg1Z++%&E2i_}g9|rcB-cJAg zU>dAg(~A^7()CIsQHc8*db3QY9W`>uY8$T z4xN8)(rJ`M5HEr1Ga1d__K}od-YTv#s4ki98KIuN8u+U0emvB-r8szDO=HU>yT46a z%Br9%UQF=St5*$0rUl7aS>x{(nm*MA`)4N}1SAyyGHq}0riZO23wAg07Yr_Mud1Iq zC3vs0(r4xSNoE*6L8q@bGu`u0yI}6Uy?v8iM!s{Ixz^T4tBd>7vD7Ec2!qC6xi`D_Jt zTRE_Y#09=tSxj2~T8wh*>F24373he4hu(I3U6HcO&K`IZ*TDYc;X}##xoUnULQjQQ zX+oObt|LZ9Elq8z{@(&0WElv$Ya~U21FBDcm_TlJK$`;E@5U&1S2!{nQKJf%MZ2=mRRUa zzC}Dbk^@V>L_rrt!6VM#s`LCa%Zi;-g7J&SQ9_H9(96+E_{b!4?)2gf?N!TE({?X@R|jOevoTd>9wV6+S=LyU_m6iPKtlrqt1CsW;l>*)Xdh#{M8Cf z5@t@QGoyj%NpN`MZXqnADmX*wtx<2Et4N%n$T-?BYm{de3bjymW!H6~Q==LK*!&W%8N%`6oNpW-}10OG+y+? zgTtlw<1SuosmGDdz0()JZdrMXHdo2Dou}Bgj_Zm4%!3@h#4TlS+euYnVT{qiHU8AJ zv;zUNcJE70bbVjv&`5bHrLBFq)T)_7br|&`aMm~bJhO{Pq_Xq9)cKPS8oYOtmK$)V z)1ou(krX$053^bvI^Pq3OO|%p%_KCvu^gm8aeq7o@YGv#3u!U3{k> z@a3aKWJ~vYxN3m5Ark z;xvksd?5egLClM`hf-z}&1$w6mRNi)E|s2q;?Pxk(yw8L>dh57c(k_Fu1(%_~sgU&Jsn=W_o>5U46qk>;IQ?*Yv-B{uN+Dsi(a$IOmP7Jdb^~+X$Rm%J zK`5ochc{_^$hTHaUTLl>#+QF|sZI;a1zxv%o`6*QO|#Q-$YVjs<%7%KTuzN9$JuA> zEvM6|Q$t`oWGVTVeqgL*Sq@wn9bD-Td$_f^I(+d-)v!0u;KOD$jux`_DVdorD{c30 zfeV|MGDx#0cJ=u$zF-&{rT_88OcG?e>e6xtWmZy>Z#wc-1GLK;mDQ{NUQT)L+k(cG z+JP&2tZ+p}%!-qtq1hBhn1(6@JlvpAHyYk$ux`UKe^n#PS|mGP}4}wKa#s z<|K_-3NhXe1T4_RaBwL1_$;ZV zlj^TXdkWt`_UWN_?)VzmqxoTkEt?B)i%-+F&=dDPVs#SPAN`nO2qQchU*nz`vv?EQ z8sqVH*k$@q;EBn4*Rxp`XEOYhVn2tIQqZ28vpv<8%&Q3q>iFp9v2d&y;m`NS0o4+TAJ zl`8N4r@g9mTk?m^0;ubqBxAj*(2)3Qo=q><5n64nUwvzYw939{UpE{2QsJoGGpZ64 zp;~3-p4=vR`9wsF*r zmbu9A*LcvvO01?+FF4bP##kkqr>UFXPyufynmv1FI~-VYO3Pqc=A zeoZs0Q<^?-#Y5+U`mCtwg^w!adJ(x}@*xFZEA+$;N|}ITVRN<9h>PH<*I8t`*Q~_q z*nPuS@5Z~zxSumJdul)T#_%N5njoc3Oa0;Hxh-QPl3RHZgll*4BFq*eZx0#;y6D386@m zY%$E3d__Lw+`qO(@SJw$d`Ey=he&8!a384OskeloYzVOD= zxx3J*dLz$BPfveBnlDq8MllX#e$`G0B6{*qB-~mEOa-d<>b;w7GDoTTu`pqav9`9R zXxM-axHOB=r;TS7lf)Z0e^IXMcxg~vMABXq!|D6_D|e2+#YTS<a6&7aMC-gu+m9aQf#j_D?55Y3QFmP4>u+-#HDH4wb|d z3FhOkQ)3-}VJY#3AG5onG~lvGlE=qh{28{VSS$hW-F4_&$|JmfFK^h*jgUwZ20_8; zjgv+WAK-G@ny>z(i6*bV8^XO!4vSkDlHE({nM zWGQ=SQ{ttuq_AiyAs%^BjXTsHiwLm_)97-epj4lpUZ27^n1g5dHzBK~A}lj=v$*qB z>GXQws=N|Z!)BMR>(h<<H5y zrYP?92lofb$y3apJnNatYu^Py&Nr=%i-0u%uxIkhF(vgmz1cy!oB35vlu{zg#^hk)Y^$fhMXHB1~I>Mu)bG}qtoa3L}Q)3)=7N$E${fZ3UX)mn^_kdQD42oSu=c)}KHbc55u22s}s zbO&a9YvH4<;nYtbrpk5~8hD<^Qb*5!vlg8yg$ZxYzmT}x+~asA=kx7XW`jpYDw2nn z&TQ@Z&Z*qr>or(j=K)sicM$#PxKn#HMY|e^gfTR^?*C^O52iRJd~a93G%R-m{=_Ki za{K3fg;Cplud!@aSQu*T(W7xGm6`4D{S8X@h<(^Cz2`qN3T&6FkrAk>M&TP}o4@-s zO;~-;5%)eh8|K*vAu*Er@S$vpIf_`j(txeGx`ztcJ1x==P{N6GM@L7ipLL}QV@1~= zwS!&ZQhAIoomJS8ja=arDTI@@7n_`~4{FDFUH3G_E+#d0m)k_ih3ukg-;H_o{y`aG zs~a2~v}=J-4zK@haxSo?%JKP`SgGct2qb>>ea9o0^TX&;3oj`qwsMHp+aYn(-92$n zPtVy#M+U)&w+@~!)YR~pU#6|P97(+&HEx96jhztuxlZunCQ%X9%ZW2GDhiX4k@31o z;o;MwIpo0zaXpNH<@M??Plt>3X}@XrI?YE@D+n)C$z~?z*^>KdHZw8#X|F1dW}=m`^A*4m*UrSKo!FFz+CLlz@Dw6&?^hxcYz|oXZ9VHk%h&4{d;(c zdf|(7YuyxiJ3H=5;}&KSSvZI|YUFFy09lYy@+1yR3`wyV#I59rf096b_;a*G0n}f; zXl`z9tn<$3+RsAeMxPsgegT2_^z`2j#vb-2{OPcbai5EeH-Mb%YdLPmNPZCl{AJb^ zh&0>bFNuzUfk#4t5i-Conr;yD*~tJR=NQu$9M$1HH@7Nms_cpmb<)Ko#KSXPB3KU8 zE|-6NxWBh7AR;C^B8CM*74`wdwjD4z(EW7apk$j!XGc$B3&^jvDB1iTrA7UezN$$h#(& z-}o>u8Me43>%W8szbNECuH=wWAcs`oPA2F&%gj%Uld@j*N~xcS#R67rK2adgU3GZH z@&ZYi^XC_Dcgf4(b?y(}$n*2_3odY}#XMAxl$U(vo8G}EC@L%W0ALMoxkkiiJnsO> z0w>g_L`OhH#kHqROh}j{YyVeP>IXbzyW~!R#y2=y)cyACshiyM=LNG=Ehb|<^HB!<9Xb6b#gId z97!CGE1Rz>^*W9IYxcK1LZM-bQ(UB)`QFVGF)CU@#yk zUh5oT#p|+NG8)3^=Hq7eA+4tRBt`^}6~U2ky+LdToN{OI9`D&$_R%GAH2Lkb2A*t5qL|g5|oqW z&hu~@>;KGkg~Gm7+9onQylYJI@jx|y08IdbMe#IH#>@?h??H-My4V%9LJ0k2e!Sd< z-B86IEtB7Qver`rQULnqBvKt;T89tfjta zGepE~k&IXm1Q8MBCyC;wWiDUe(yk?%{+QaNj(#4bl*mjcEUZ^2&c=quqLRb7oST%i zIq7X?ZqDa<_7>9lMZdveqYumK+Z(GV8tq1J%`erA0qC!t#{V5jeuqc;{t9ig{z?=`u2ov2Nk)Zwet@k9o&Wcl6 zn_Jb%8678koKO}%=&twyWG&2}V9n1HgM4%CjBD&vMC%jXITpY&=i zr}52Va#*(1VLRLMRXtjF?!H;i$c_DinDojiryXHnVO?C;;+HgNFikBQOKjd*y^eA$EW(3nV=%h0Foe2j_EOlgvw~}YR8o*XD431g!os~^kbDn zTGaFPzLKsA$ReGI5X!eLpkXC^AC^Sj!$cvHm$T*)zCyisVjkCVvqckDe`g6E@#DCm z#e~GfFDlnApKGSReL#>>E`zXB;xG|PP3>AqUViWJ(1P>0lRsK7agzeIQTzyJup1Q% zOCESy%uBHM@$FKZGAxMD(z>{CQ9UrAo03V(lXiqKP!FJ>U*Z z3ra0r&E2vxInvq9uHt_6l1-i)$bd$N%7$XE{VCOe?FQy+{LNm&$7eNOO(!CD!zki< z6!kh=1j**jA;?m>F$H<~n$#x^(;axT@G;i0>^mzIsZVso^z#_Ull9T&6ph zhtC{Vp(@IiTVfv2EEF7vKf7upIZrXbdeMMh0utAVRE!kVgR8j~!;nqE=W=q_O)L*g ztcFlPnF$sW5);KQe`6ve0FahnfLDXtbrJd7?#dx(GYh@HyA-aA^FbQ^dVM}Z_SbV8 zvq&X323)q$^l;Dj_hJT*hQ^rN_p3uBZ#jWg=8RV+R1ahImDl9~TN&b;2D|9fjehUH ze<{O7P6M7GIGn6Xw+dKdzF}fq&2Cufi#c2ves$eDFf#HzR={#Kyq~7OTF0pC5u2K` zp?#70fKP2XVsHj=#L2`SrqmYfJ4`_$f#v`RyEkGbW0u>z2^s?2kP;qo@)pY>B%3?t zzke3$*4vO(5=^rrl5-B50rm}p(l-qsLQWsA{CBtd?+MS(&(-2`dFHi)M)#8FR*4`c zMm^#-v%I-tqC!Hcv&8!$Y|q^reeVR@b0w%K1Z`-G>Y878cszsc!Jr}t+AY({4-bRN zU`M?@Ina#KSWTg5iFx9NF=NMfnQ&j!}-fVEmL_sVx?YZ%&ZZ>>8BwFd_VXzP&* zly#O7eK0#`;@C+_HQ76MqK%t9@B}PJI#YgRc73f_7@G(V{p6U)Y&G$fDMQdUqVH8k z_2S~t>St7FerJC%qFs&9@baupWE$1#>yXTMjH>nbQ9d!i*A zY7=Nfw0Tu@>5&gv0utvtxoLv70ul3ea?Ot8-;JoIz9aLoH$RWD;HoT z2=KvrkIvg2icTveMDnZXW!Kr(aCow{;^Y?{oHnlu8K;$YxcKRexHxEVFksVSA?cN|`QFt9vb;%VrKolP;c*-C(oRP-c zBV8lu0&2V6)UKfL0k|wLybl|(OrItT)yk)K6SJZ^ z+WtC)D4BzjI_P^>F!M`=!Q$dWg_2Ty#cz+TA)-IWGdL82c5+Kz7h+yl?Y?(mwrDPT z4Xnw_@D5Xm?#ZpKVlY*wo0+j!mrH|37XC0M9x5&SQFFF4S-tdodC$lEcU4Ad60Ypr z28~j!f*c$e2)X{$ne{fO1NgLT(18I(as*mcx)*^r3B27&NkZrfcDfzZ*R!wrXedSK$EZDkqMmaRhz~sRqPK84RtHaepKAHcowKR-zE=j zeia}EGiGG;8EX4yfz7S0wT)PbZV=?z%@T}^h-2bcltyHNoUFm^PXGa#wXVvSk0idn z%(M^FcXZdaK%K2y z9rhq^DKQ}n-!J{;N1#B=x(ehyHoi0js$biBw)%RHT%Rfoe*Ub%qOu-6@yYNZtMKz@ z+=U$6BF-Qx_qX*wIiQUnrsT#}#s;MWFyg%cusYKCK-YRc;wjMUlRvQuqXGGOts;ST ztM#FNvQ(esSI48#nfr2$uc$;iZ`bd4UpXURy?glCVH!@OS_2j?3IE*aUsS@$g z>5rk}^Iwy}B2cnRZ3Qss~vlg zNA9;s2Ce7otwTX|o}YRz=5|18M9v@Fh`hGIrV+Or5^c_b-o{jseQ;o-mYB#6Nz-X; z6SN`ai=Lzc5c52mI{ECNad)T~vc4OAwLrV(gb!weL9)I;8-FSg#$EkUc~<(q7DMy}aFLbQ<6Ck(sLcZ*vH74$ZUiji-CCU2w*pNVS$@Mcv29 z=F5H*;Gex4JB(L{`>A`f0mwfPPU8Ii9)tni+n#RYdQ<;>M6Z??Kt5B@8^ZS=J03)q zJT>03{*Rl1r-q)vkYw@8D734k0F(?hz2o89D}$}rRQI!8oARC`R&!+>wFR2w86>@HSX z!-|_HmNYM&mai}iOaC`LXj&R+0X(R_4duR{R2P|fcyRO^Y`SVbDSR2y%~z#mW=6Kf zC*ZS!Jwi2EXuJsosf)h=WYlT~d{|5jGdt}3ZsWWVddRxD*PmT!8}I9r{1W)Q33l{C zd|4t$hcnM3_YsuGqux`|||>2M0y zYnO1D{Od@dHeNg>u$bdQic3lPm?>m0GQkDbZf&>R8eUiTq*U2J2SV9@xbqC#EL6L^ zH;vDnGMx$x+Yj3;W8N1Ppj}RsthGkV4q3lkh2Y}jJ0W%we0Bp$d~@?H>|hwsunM}m zY419)z^cPQ7qH_G7Q@6g3-wrD;(UE55Ctq`Srqk83`o3$o#<*r2R(hfOs|SiKwPSv7={HF-z_GVf6iJJr0CB}g6}9>eVd7v*LslYNHR-iXpNm)1h_sY*E7d|Y%bDJ9Rlg(9a(|2BG3-@#;CCpuGHXk+T`r$ zv^XUWqj!2S^h`p=r?#!Ir4FZifG0T!5gJjQ1IPRzcb+*N`=ddN+K_1!-YA7D4OYUd zQ%%3$2@?Mk9H+A|R}-Z^q8J8Q;UoP_Re`-%LrMO%T&0Xj;CpjkVF)Cy`2T#r20R4z z_uu<9{&-B@{=fHY{J-~W{J-~W{Qv#^8VA{)S%kfF{r*b=wY67YtD#O|fhcP9YI3I# zc`#+kkiY1ix{!C4 zzrFr7I;Jd^u!B@%cfN&;S?;fsi9)_G;$1>`;W|rRR>{I9hcu$Jw?P82gR5TpjWL8B zbFTwgkm}2&MGd6=xg``fH`*cMOUFV9J%Xe_8fOA(&;n^u3&b6G%T9hb5TH#j7M7NV`5b{`ubU6;aFAu_(+q{3hfX&RR z!R25UZ)$34_NxU(z3qZ!;lA$dCgg1rLbcP@kTHZb&hCyORt(?w_7B~^CD(a_;c_K> z9X8_?-uxDXbdDW1`V}cbJ6<=e<%d#7*ex|%cnHHL#>xp8<40K>@yQpd$_I^hMcAG1 ze?MX8;ZdkEYU6JugoO_wGfWeEdU`hh`1Q5z3*7;vLbs-X!~6iXkMxcAe;H4mK?&dM z>S(D&Ht%XxHy2Wo2bDKvxJfV*cw(0?1%FEiElrUtL`B;Rtalh!cRA zV{-qC)I$^uL4gfkDQ4`>(%(#_+w-i4!07alXO4vbxT45{)F;>zA5

113`>R)Thp#PZy>c7dIp`ydVdPiI%{hwLN{P)QJX@c5Mi}JrO&Lpbv9!#}5o!^sxdn6qJ;^BB|rVfWnFZL{je`Ps5&n`gmn*&`NrDc`!Ipff}et0-L#M`uzMG13@RojEqbI35?Jl zkCXgRCti*~R!o>RqmI$R?5-R|AxfOx!p<8Li=+)y1U5m5aZE4 zgGEoU)A(M~*|n}A4>P^k8BGsrZWal(l*bK0V*|F_L~3zA?xT4gEklc2y*RUTzBZvi z?P#KnDE$^47N?)(hObu|>~l{@op(mM0uWGavWTQ{-K1c{syl6^@m;S+^6-=Q?3|mP zC92FnVF+aP`M0>&P4Bu9TQ9aV>!Y4pPjfyId@{>V4DeYBmJVo~ZyIq`=F_@XCTGL? zY5W)KPf-iv=s;YBWmT^Q;hdyq+Se}yA8oXacufO?%pEg`z;=COb5JdMB00l7etO6b^*vyD$6E8>4S z6drO?Y+c!vnkgvk6^(#`Z8Eeu7-JMf8}{d4%!^%jBIwq;yw|RLYJTgIUm$}hJ&SP& z4^nsZYoT)On=i+(ZH|1tUelEdxVBjHNK&}L8$fHL%^15IPM^CLy-p8QWlfs0#z!;_ z2H|K`O!>?K^67buoU?Q72`N@|^a;km%{R%1u;SuZ1nu0?g#0i^t&zqnBBsWYhXB+> zja@sxn>aYn*W)es-%DZ(3-0NjVL_!A09FWd8?fEzH^0BV=)Moi$K9Ly*}mqWtBaH- z?p=RE333e+1fQI!`SeJ7n!vjlhet)mz>oKLSjXXI5EY42L=a4-fv`N99rZqL3-y24 zpy|FR>T}4;hr|1I2fDJdUO8H7Q9_~(qm%~Tsv~eaS`06*YQds_pK6-O{5mAJcBG|=OlB$#(8O zO1UjB<#VZ3#xC21hD3BJTy{cu0CMKAH=Dd{yt7+-h$zUDUOGzUZYSHOHgt=Otm6CsanD9GEaZ)G(78a`h0SzW{h$i&7;|@ELgzM{KEG;^gI>0PkWc!;J{pg{qo>1f`%hArrGhh?tHvwWPeQiyawkDs9Z za@35Guhw-lq8e%(JctNfjT|5RDo#E6MM+Zh{QMfe4bLzUTpaN~s(mxes0FStPr{r1 z*qdtH$?*=pQz)1S=I&;&(A$RXG{>#?gGB0!sd+CpY&g9O&?!^WUsbf3xI1<7Eh> zg;_-q)f`jD*p;Z>c7`u0D^#h4qIZ(^++VF6ciNy9_{zfK zn+{Zo07w5@0>Umc$qB2;iKxfvlN%qIkOR`30c-j>fmX++s}h@I(F60&2Ku! zy*MlW$G~Xx9kj42noUB&)0ZXe9DH{N50{_Zpb^GM`}~ud*x!>L?2(VO^DLlj$>r2q z3=>CnY!RB>Tna>y0se{F)Ujbxza(6)>qvQL7W#|kXZ?>#H*b(nl zv7+x$klW-Wi-#ScqK5XQ{n`Bl>+}o-7Ayh!_Dx8}{`cP;9zPZ_=ZpNE%PTEaQbh6q zW$0j`w*OzJfgrI5G`7bwLy^WLc@{fLK~{I|dC$pw7BQj4?KF>-JOXBnB-xy+ISAb3DPBee#aPFQ6yN zM}ns4WzGStuPa#c941lNH`0A7a|Z_26J8tR_KA*GMYMZ6CUIvoJtjaKCKCv7WPoA& z?~bJI+1YUSJw;~5SpgW69#;d7oPwO!b%^keMGp565r&01CsV1$>oE)VBu6x;h`|jQ z+vK@pEG`{sHzNY5Au=%UIw{oKR4f$T6a{AifXX)E4ddvKl9n=I!i2*f%)s?x3j0SCEjkGZ4tkvH1=|MG^X(tq_s*W(xrSkV=f$tG z*cD>t!6LQPn7fcoEqX6LJpr+p@2Tght zc?bqZmH>>>&L%a0H8V1BM}Eor@iUQPGtkEUs{S3xb27SMFA7qW^`#6-R}BL5A=1z{^M14Af`Co-i#kYN=)V`Z|D3-FgAxI!!RUv2F>j4OL98#%d7RAO|{T8oG%$_l!0_*_ei==NeiRO7WU5{Z!qorueB z+uO2@{d_8HM^$qnIpr_IBvvJ z6t6}yf0GB(?~5V}f-M-Bj+nZ*zMj~9$(7{x@-Py>_PkIRr3SAsWD^nhFrI3mVS~nf zk#4c0rW9=rp6h}mV(eJKQ!l+x;^N}M!?F)Es^#(x;IF#9fO3xKNzc~3Pqa0+Td2qC z3B~CLz#i+k(Kjb(mk)#XSxj@xjlO7t<@y=w7xxjX_8;(@i$}7AYYkb{wkfQqD{7{$ ztr3Hy7ae-Akkqg<_5j*aY}F_wo^(W0gxo$SLW(}jo2){`gGdAf543q-nmn|6UaExQ z(le^A&!IQ0_=zqT$>#rr;be9EnB*O1EUeyi1TD!Uqll?ndM95jNHrRgs+zJR;} zWn`P9wnl)7j7K=Y?C(Cs?!!S^M0ZeFQ89AL+u81Wck=gqOw~TL=3l&2mJq&MU8KwT)+D0;?A(anxR<$@a{cBZIMg@+v~@M4_nEpRF--1Plb$Msm2 zu#7K}yfj1ay=~VHDE@h!Be{~fl+|LUyW{yW#^!6?Au+~1uVG;z)_Z)l@sYv~H}xSi zqeQSRI;;c|9UYza)VTVnp|>%sB&;~C-ec69Ms))LbGX_7Zu;m# zDHHbJ*D1r;-cLp#u&4Fy@KPQ?t=P?@3B}dYvWnF8b69lj7xu%7?ccb5dJqUJJYyca zT_`nL)vK{QxVZBTuRkj=;^R5Nx8I&Aw>DTtg9=sQuvc0v;uj4>hp1n4Ju(cDk&S(C zn2IFt>r_`t3dZvp^`tbB()c~Ug1D~WcUYBD=;z_$l5enEqHOz^4O@d}U;%66O;KiW ztulXmi zBdRjKzJ495f#h@_2k4|D3RVaEY5Ie3BoD?b!lq9=#CGRuS?X=()(5T?C>`Z8zsk?4 zj{zweMm$?%j>0T{TcWxGYO$Bdwop01lVHT8j2R~FBSv_ew7-v<*92;AE(*5=@F+&F&Vg>(|Is(U z{Xq))`H^z%{00|qwoKT5Su7YahLrhW?W70C-R3|#2w7OrP5&dnBE{~2`#Z-gLw7V# z&yZee?n@Sj*)%P7_)8Mkb=7WQAQe+WD-Bx;%==?vq7Dgx`jo@Ixm$VUZgs7yX-fZa zzmrz9ZJGbM>fP>SW7e7`Hbjf;W%Nc1L=lO0p1ge7mt;7Fn`n;$Gh{cZNvw+oHLs#> zQau-xU~K#LQ{+*J9ZCSvp9`#KDp|R>zTMwB@8*Zr3yaFNfPg>=?lE0-ygQWfB{NUD zPE`ogHX`J@mU|mR5PAW-iz;E654*kMAa*;zFx&3iqPha+vSub7d>_n!moGOGO(C=n z+g)w)T+Z5MZ*uUGFES#+hNe~(1`(=r&93DRn9m5YHP(%(>DM8=eZ01NnxL%JJrBjE zvY~N?Z4qvADm>f}Q38|?cK?o$-oNcad(GAaOY=yG2gXxA%ACA0Kx%KdA%pEc&69Qg zCM&>@pr)o~dY9&XdC=v#lO4)!*t{A{@Ve8npDGmK)OvLfw(QPmwwjSq%%0or@qq_E zt#lm{G0&S|EDAx?3#Q+a9j7SpDO6B^A+SwFeVKMnR@!p>FSwX(-s?ZNvPxFJT4 z^^|Aq^zNROgDa%p_Rvkpe_71Hd&$*7|3D#3Lt8s#pN;h7?jHOpsjBJ+n0MOoc#nfY z%n6;@?JJvtC;axL%HeXsH_`)NXM)fYQ#X}>b$fugN!$U_C<19tc4wIDLkJNIh@k2gN`=PA&rEXvlA3L#>s}colj03wwMz==~l`n zmnF`-ik7d2ssLvLK%}OxZ|(79F0NkycCO&`JAvScptmZUNrB<|`OV2H0uJg0mI8g< zt$h%PW<>Y^#L|YR+LgKiu=kX#2I0WCS>I^3s4Y!xYbzypD#s@xzvjQ^xsTtWa0vVo zzHuN)9pXZ1d~hYvnIF`q{TF&!^56kq9mzZEj3Ee@tMW z*OZl)*V5k9+d_PgfdpOg8YTY&?LVp^indOU1Bjpa4G;80Q4r z3s}SC=)>eb-Hz2)1YS>a1#m~|!^7@W#-|!?k5|kKy?XV^W3Vp+qv`edkGcZWE=_pG zFdsTlfvCF*sJc`KE}e(u*)F}*HcvY>G=u45`%m`=zeW`l(4X|WkeQ=~^Z@k>e%g-G zfec`6&~-h^v~Taab=`MZS7H%TfIyX@b+-xFbZ6cBUryp!3!spYO89s@uaEsl7!gpgSKoJKMgfM)i=x*CY}32P zKojMsJn%K@=ek)EG0CT|1@HkTxB(`-_Q?T?jPU-vVd}s*D~3(@cb7-L52b z+~8L>%dKQQ=$54K%2#74q~tw?o86A0h9c01S!T94fOnILh<4|j1lWg$&tEuUegvkM zNx5rl^x!L)C_xbTVC?gFf7N(CDhlft_OQ}nJN@0Z7x{a@*ws${-i?Ze*v+mfBL2`* z{9De+qfpFk7>r&1o&y*KM4-#wPo(6nt)`rq7-L$w(~Vfk^(iK0%CuK^0el}}XVSaq zT)9Y|Qh;dYvlt@O?_*E<1sO!6bsB6rZjW@`pGLcIZH6j(>X8AegfYF#BAddPl$@OZ z`7`1V3FA>^_B$U5P1I*bW1~6Ic#l{9*gwinJpW86zz*s6_l>!o(ljhYD^PnPhEh5y zW`6YqwG#=$91~FSD%BN`r%+im8>ZgglHZePL|a>jL)YhE-VtX`E=QASOvD7I2YowN z`bXz;JGmR@V-kR~hK#}9C=pHof{wg??ZV3u(B+K;W$qf=kw7(oq&g$HLOA;MHZ_Z} zd>@%nKKq05BfjzvhfWakgCwFVD+Bm8F}awP2+#c>)#>yK0!b5gW~TPpr9-jQoHcNr z((;VIIKJ=#- zVwe<<#Tx#QK&*{B(N;IWv{%C;&&;efa$v&`N6p5+p3bx{Xm6+1($d2IQE>vBrJ&7h zTuaGjeea92Q%*e~Q-Enjd3+cm%;#wU3>9~l zdlnxooHDs7a*_M*XJstE>2CrtqK(M+EiTWC@p(E)^9F;X;M9(q3Lr1xNFgBsZ0LC< zhoAPG<>5$su3o*mZmguvJ*w(2M%g>H)gUN{xEEs8OHtEO3!slhLHX+9mHAPn6AtLL zG$D8H%9~%glo?v46N`&c-bk`e1!+1rfT71^VO~$^7_W6>@O-yOHT(!%SWr?@a`Cz~ zhBWEF_X2oAH_#!aNOJXafP7hE98J~NE_clA%Uj4JEpn&YntyQN=IL4O@iNoRmE|o_ z(D8VyGg6IEwibE>QS_>alU6r-o-2(^2vLRT1tOARqUS*plzp+$RG~%|7SBF^CM2O; zBvK{3rpIeF)(>^Rybrr@y;CEpYfr9ZdJz%1=GLONG9LHMTq~%HwmUv->o7HvnEU&b zt>(iIK@ev&mj`o+-}c`tz`fg7%~NCygb@=!4S0yT-UQn%HKWDK0~L*Wb|Q;|HaM`R z_+9X(sjiL?^%{;6B5zV z`IKMEW=#jzv4RuZn|4xfyHIU;@gYmlqgu9>9h01#oEMO=3d1T;S_7nL_gml~Fo{Ge zXhWL9Z5YvmEy}q+(*O_1Gg2?O_T=koshOLtQh7y5^|#w4HNy`lnHw>EA@?y-cvZ~1 zE3(8ww+R^d=6rg_sdr=$vN$3|^iBh(mt;^g7kEat%kiItglyYViu{a9aEXjk32aTdZHLT8p2NmAvWJrd6)W9B1+ZX6KX6@8L3E9eD}A7J4YUY9-t1 zJ+M&z#G1+S5<{l?Dc+7Mlt*sGtC+EggIolOW1PZMtz(8H=7)d?Z+!cSfR;}&f$;8S z0$o#D87i+-1le0=X`zP%#vrAL+jhh%<-V$Bx4xqFSGQY_AU8v>MWxAxl-*lyxhDM^sM%C$I!^(P5rd{`BsfM$7a*`?) zq`>6*Y?$Zt@@z}S_wdh) zU%s(Y9T6iOkI5-Xc_J8dm-H-T&*||-(gpeh<_M8je9p1jZ?-cHR2W!bFWb))wEenR zWwHl}BanI<8Gg0I;(}PrgFA3t*YTQ4v_Nsmh;c|-i-^>Jd+=F{{c+q{_LJ{ zLVhT`uI8)7pp3sP)Vzg=E9$i%1AI(UDduwz&2*t3uD{7ieV2ioze8h(A~N%h zUe~4xbQM4mDfP8u$U?8WL{awm-tTZ9jOI0}J38`}F`?bzP~uX5dfL6l8<-#GH{#gb z3JPIm+flQd{%k3ek^lXLfePI_Kq6nJ^CKlPzZ$yGOppPE5Z9;LjtVCGhVMzuPb^69 zss_ieTMW7XI*vQ)dR^E%j86DYCb!koyO(l!rru_GHyV5te69u6|1s?J5^YuIH~-_R zWa0)k3Pegf8!t`!VNo|ub44x@(S%&KJ3TpB*JX2!bgocfk&N*=T?rf#ie^p=DRIjywrYCZ}L;oi>q9o{IdbDKtgjF%}M z9`^B1v~!+ECOc{`{8oADAi^pN|2d+>i$t+&!SEO;rO?rvXPvZD)>Uwg3BmAxb`2hP$!v;!?w>$l{i4i^X? zq29F#3H$yQts6a7{4!0o!!jVV^|_Ws;nuU7=S>Z$SJ;vSva!2`RkK}W7E5W$VO6t< z884n*@|4Q^(e~S+@Vo>tn2oht98$R3F+ z4NHvonJ=o?dVCW%cTn!+G8Eak#^+%^n8V1GYFhjHm0ymqH(mq|aDlT+E_t<+#oDp; zl+z8{Toru_AeCg0c{MUJGE_lYCyGEUhlYmErm+*?rRv@BC9uQBuqjNdu7J@h5K`N8M2*K4?pxC-(;=ymco4yGhTOwe_JaXPyey1 zJ;j4ztbwoU1a8AS%%7RX*<`ZeV{-DwZ@Z-lEmft`iV~nXiS`!o*A(SgNmi3}j0P7n zeYs($nVsBhMe8r-0~F<@V9z6-q;Yzz#_qm<+iS$bj#${6>ncflt`H+t6{F?2!QX0zOVGiY#)yyB6Tvr$h^j5@i(V$KNRm63L0Htfg=^9b?4?wrC;gdB=*ckN4SeuS2 zwmh(~o#K&oYNBgeWJU>aM*Qs2YP^nr>k&lR;)6~km7*0O(PlyUJ{A|f#M|2^h`QG)v3URGEVA9#;h5)j=mJx}&iJC6xZ&l@6y(!dipwYJ zM{cDEH%2r2x8QdCN6NUYmI)F;G;X+XeJRL-rj;)kFS1tv#}P4%OnxPAf_2fPi+ zML<~@`y^~jUTUXp!xHmkEbPVwQWW1y-#xbccKW`RjZ2w3w&sm4<1Fks8y~~3 zYLzn*sT;@!Kb_!TB6+VXK^=>woCf0Zc98q<*X}@B5BKVgNlKlUzxu$qIapz&KB7-+uaWc2AO$IN^~4G3G-2tFwZb z*D{$yIkG_elo3VfRokoQZ}gk2(;u_c^C*-rc8y&_jSmh>C^>#{aAf_dqCR05gc54? zKt`Sw5hFvDx@RU4?mtVt9#RRrw?;uLOt?#26i9ebGPmWhAWJN>cIb`08!!1x6Uqam z_8WD2%59~OlB9J8x|YW|;<{~KPd{RknJtLTlef4Zk6sW0fI)yDf2x|8oJ@M6rs_=m zgq(tc%ie+#(6k(UgU^t;+8Q99Y_TV1I~3QA-@h-l_}!tQhgOv3&-`N1j`73hyhh*P zqiU{f?2WAD=~T;bW3S431JYi(A4xGYRR1)yqC#(8g?Ca(LUD>YT_dUB%VGwCg({hu zP{$EB$x+1~L^#*(u8uCJRJ zQdzwpF74lt?(n-mx!#D~xY@~df9qYg^sEvR_eN@VP~X0tElH0OJtTO&NxRU$VK^jo zlsam(aNFimt|z_Pbre_Mos0<17N=vcR{3RkFXUUGxYPlJmbo7tVeqAKqkvun8&Vvc z+=a4I!kbD3nF^`6l=0NJ-zsScD#kiOj$H#2$JG}mY2Z2sbLjuJS744kE%rxnZ&`xD zu*S!CVkcpY2<=zP-X-PbvaYV*Y|pKfH@{NLFyhx7Dz4t%dfj_!X%VrrvqK>A^75n- zKF@Bij$@bGykY2K1MLUdiB}tppEJKqGZ7X` zQyJ%m6#S_6nqtp)?v>4t+mhoJGCS)aLU!4Y;JAHcbPbw5A}A&Fa4w+r|7l!qp3#l{ z+ofot$e;90g0b< zq?l&Ee-<|W`Ke6BCjY7hg4!KywcRnW*HKqcaMAd5WqG4dyf}CAE0*nH%TR)3tpA{1 z1#(CC`mz4aL#?)7uFu)bD`(;FTipZr)*L7NT~0qraZBGn_TWgu?l}T5`Qk9Crm)5M zckR^UtJ_0o#Q&n{s>7n{zU~a&(%qc`(vs4MARsN$(kYz-($XNIbiAN+cc-+Jba!_R zF!SB{JwgEBkNu2H#30rtaC=*!71D z9cvBUhcu#~w@$?y4%avs^xIzr`0x+`$LHa;qH{UVR)K zJ$f7T9YxtPqnPzQ=^vT`sI7Rw8Xbtu7~Yq{pvn8!_GaMq=aS)G$k7%eDMvga=W_h26pI;tKpqLtF!x zFm$*`EhAK&1Usx(wiK=v?bqQ<2_Mv=U$RdDry+%%UrDxT;YQUpHX2&uCe}@oqQhUYG`26gxV)!K zU&(tWp&)_VYAmB1-E345Wi_bIPa`8EuF!+xJzq*$4UC{aensj(7_Z?;Cd{`cT1aUl zv%YdE!RTJyQJ=&IjnD1c>_UIog&KU-qDxEqVddSdblh&UOc2TGdLT_1E2&q9-U-zc zEtZXnAfDN4xe$p!*Y{sk+?r5X{Y@QgIW9ZgFw|<`%38r*Lz;cDn2wE)7eRpYUnkX@ ztyw+eYDYZBFd_SY>xoLVLr{;CMOfNPxp15}i82R=sfgDZpel<;&oaaM;V+#o%#7N& z<8LqUFuQQ2#l!suiTy;xkOxSlK_0xF7jAtq&G~(v54Pbs0@p2aUG2Kxe+@;gZ$k#$ z`-3@J=| zP9T)Sv#ub-_4afny0o(6wf&HHv*mJ=YjgHjzPLTmb`3eo^s*`Wc`&}%gJ-EvFoam&Y_rNxax@LAl#^ablzCUxgku}j&5@)7C zLOGwn#44Xq*)NXdA7(p?m|gOaR>ju$em$aEv4vOCy$)JCiJvW%vkn1I0kCj{y#u7r5oQJj|tRyFY(`_ic_$a&r0Xn5(Rz1E9c zY7Z(Rk;0#YHS>Wg@%a>D_ex9M!UT4|Ar;AY6Qh13M2$w`F8TGMZ}mfyMo=nI?yq6l zn%T|+=+IstJhe@Zo?=Zn0zZ92&H`Q-pj9`SBXIirB3#tyHg{e&zi8#Nfr56>oC##* zpV&5+*Rm@*?5-33`MIUt?{452$c|>cX#TK=Jwzjb)crcL1b~r6;ZIk%H_s=z9~+&# z_wyngXatrt@IZ-;65@Uh8iYB&=A8SL-MM)C6GB>lRVI>T*ywGviuKUcKV=5_QB_ZfQAdr+Jc z7yYsYgGSu*-H~q%_ttUk#K3}ePVDZk)o6}19X2U<*B~7v^ENYhp1+fbO`SVZfOcQh zl#$n<&4vJvTGX^TXP>`H7YnWodU-heNd(Ft!aNvjGQ%A#FpI0P=EdNoT~I<4#7^I8 zev)6kLC;Zr*q%gv#1_0Jk!vjbakkWgQzb?^jgQ=UuoTG^m2qcEeBo`58}+6*#pao< zbg~n8d#T&~0L3~OVloBUd*hO)pMeB|5;&mPBl;Y!{E%m9m>SCZE zkqg-oQ&LWqP9k)3MzbZUl9)Gs6~y&GU{`dZvJC#$lS=o{qe@KxS0%J8<+rs_BgD-n zvLz>r-_vR9>RL&x0UR}kR+6$nDSn{AaS1U{5HaJ=B;ZCzYqq+jg;Fj8ufNH4%Vm^* zkeOoA7;Bf0{tGDv!R}a=eRF(I`hOPEJ`$wNo4w-No9ju0EBE%LFmEvZm~shcP{*cK zf_~YrcxJAWlnA>7Fg=zsVLEfEIygFOo2?)3c|&gXqB6zKhQxSru40G+GN#P6%xO|7 z1Zp=iBGK3!4`nb4_V&B-TRzLiX!I2k*@+1#Lks#XUCHUv1inr-n(vwtICP30Xc77v z4C3S-o|DRX0Yna5xYwjY!#K;F5#;&CHfP%PxOxyFMwRe+!IVyd_5~Uq^?Zq}d869F zWC8j>JYD?7)?h(%GX-McZ7R>l^R0O4e7C)EgpQ}W=myI3-yGQ*TL2yV4E_|dKL-GZ z{wbSQp+fKQTc8UsyFBF0w$qHRyIsHPHsM?N1Fm@rlO85V~qe&IH56Lls@ z01gy~<~1>JZes@$h_`-DK2Em|d4VvTh9`Yj5SrHkieyFhuOYWmM|HzFX788NYY8`% zR1T;b`4=s4yCTT2k|3ddrHeucF-!M-ExQ-)s@dNotaHpr*Z!XMZJR4W4lwkHxfSO- zv0}~-n6XM0O1;Wvp3+6&py~mYN9mV<*8n;Wyu28kueFXpaT`LV3%q9lz-EH(l5xB5 zMn9R!DEmQt71<)H%r?o#28Zr&+=GgP&3j~UDC$({*Xtw(d3G+a0uK ziJ@;F-H`HG*<{@C^S@0J{Cxr5#FY@B43HeCOo!U5e;vAa+hP_4;C3MF(3`c_DRA6f zf!?VpdEmlB2jmNHN>AuxSbyyP-i49Nn>>}uUw*vwo-Aj=+UG@6KD#Vt#e<@p9P&U6 zMY8Uu@m$5HYL|_kI-=V}anxGupiEE?mOze=;y(PxJ6*W$TR5$;ChgTvA-O)1n@RJh z#gZQ!7k$#K$b``VK_Xl8#Rvi?kO*CwXib33|9*ufm3723qb9nuASw0^2`(6aTA})m z(V?7hkjF*x^8>hdZ&xbjKNq~DVfd4TnL}wm92b7OdwYQ*Q=Wm{)twaM$Suj_VEu+Z zRtE99aVLDu``P2k_E`VpUZNVgMbqXtt}4J-b(a97ZMNFxgOJ=?2$2XO1~EF@I5_lO z?u_u(Prp%vfrjTS3gqr=A=@3S}lv>=U2g^JjpCRdNSTjk!+4>elsE5j^ zS%HyY?)?Idq`T=*gc8Csc~6Cf&+-~==o>mk4%0=mZnmhcUsy6+4bAvfyY>B$Dj#(4OyGGm3srYhQ*kM$Zitfaaq4?sH8Iau$=RLIQl1!sL95J20VwC(9-|C z$kXE9B=Z&@Ddc@6S?=>o<&D2}bx;u)&nA~I?(`K(@tfzNlhk#ZZ{AO9+TrORh+C?! zxsYBHpaJPab_v@g@PGwa7cN3KkuAr+X4ccVk}R23tta!^uydP_kcCc z^P(ejL&V``D>L+dc&*Sdy%5sjtzq5-8q$NS%m&ST#H?QqPJfz+Ojkhj&;AmxwSM3i zxbH?Rk8;9Obj&6;H|Om{hHjhFiA<9w$N0U}Pem%u#gym?W9go$RE!STbig=q!IS$( zhaoQ?xfHi5ONlaxEWWsmQouO$;D>T%n5nOCMYgi3sK07oN>^h2Iy-;`Lk;S~{BpO0 zkU)`Qp!)iH{m#I4jKAY*vvx*v{=a&04g$B&b z&YJxzGeF!zG&rxt6&10ve?y8N3>iigGw2lnIptHUXa8u?A1 zF{$a{jEuL~-c4J(z+RV$JojZ@7-;5Z@(-#Hf~J6mv*dQ z`7h1*TZU6h)af%tLU|`m7OISBk1B6_=#o<_&u81e&yROok7u3e_@y9SrYgxl&L4!m zTCik(ETIk%EBh~)NIn>i?dzG}z;V=rn8Ai)5Cw2DbG_Pe_slV5g&?SbD(H>05`D^! zITX^AQZsZO!orGd#}DZp&QYZmRE7ID_2#10RY9#)2Hx^*{(P!*8IMsBThGL{auGIv z5f5ckMK7iX(H9}auwJNjeVu2o=Mv|ubJVJ^`k#JBQlX685$?$Lx5G)7U_mlFoQ>FZ$eOY41Dxomgk1NeZ++A;^pjv?jbNl}*(p zNtlGxoC$Kw)E){te7kJ=#FSZWT}1u0a7r597%55I&O@jNxB4t&kIkauH{Nac1|}jZ zBgV>JMNe>QbBy2qLj!PO5MX=vXP2=taQ}`0rEae^&r}-cq(JLQ4@h(0+?hngWgOg2-Yoz zYv{MSPn8T7vi>Tt+jdMB)#>~T-{y-aAr0+~z$!+u;d4eW`cM*FyP%>`#2SuV{m{?L z`sC~^LJ+xwgi!sueLukJFGYtCXB`+AAYHmSULd;K9g}l)bxpNWoCgEPK0oX+_bKgm zm@e{!h;-p+%E3Qr%{p`G7>)$uQwL)M^~eW-qJMX3B%tNJ7zWlWx!A{2Gmy|K?|~Vp z?-D1sLp?Lr*T*RZ*D<9mpr~(&k9imxL&1e|7y)A8e?n8}Vt%$Sg~%Z>LTH#dk#VZ| z(9dkrlaX(jqJ$t_h4_5A9J4uWm~W5NUNMqm8F<>vRhqoWnTVL0(u%NR8Lyu+Mu?F# z%olwqR!^6P1iqn;&)_9~hX)eFK8diME;4wz^^vj4JW`&q!4h5?^504w2j!$*WfBsf z!5I4Aq$fePpy-D|-5o+^vS^+A4{936>vWjwL?Cd10DZq5HaUq7tnbYYg@ELS-lgG< z#HA@wG11I-6ZlF9w(Sx4< z;)Rj8i-qf4D2F@fiu|J5T_=QMNnqf-02MvR_FH5~WGEN_io0F!`V2Y)C4m$dOuFPP zK`U|Ig)9oY->MZXy_IWl;66d3M}Wyq1RojC5=Th&08|nk;8zNtKG9H8Q4wH;0bm|c zLkfHm%lfY{Ua=rG`SKfT5}e$k!Qq*Rp>Je!F|p%SlEYncw>BzWO#@b1S9;%Xu%USQ zY9~&>EU&HNH^1z%Wga^0xub146Cr_BQ_hm^S>5MdKG0oerS>21VnvaC)-C5$m>7rj z?M&uIWk0N0>YLU6SSiG1iyqc$^8(b$^F*asyADE;V;C9!2auo`MEzA*j4{9)q@NGo zm+xN0&`Hyh8M(YkwNelT?||xQ**rN!eOaareaq`UN9mPkSHVM6`Egvr>wIK>lm7-FXFBgyZ_oskK0 zi3(22kE4h_7<>=ix`o@v1(msQ!S(s+t+L;jJp6DbZkyB_*&D!>7L~)=DltiZy7`M% ziIX1F>GUVjv@DhZDLoT;ERXL_4hp!qKD}q`38^PiBrSy_Ng@%_nsEnp=fjCJ%}-6= zcjdsUud}6kTT9NJB(Dn#h2Fk>`?pk& z_xfas48e1Qz{uyY$1{Xpux&g;4fKT)W1J}8xBS8Y`oE{97&^3Mxhk+WB6{#UEpvrH zZ;Q%s6ncJ-&y_0RYT-6yb9b~L@1J<8qwXU9xb+&yn`jaY1yVyLhW6INuy4&631V|^ zJWg+fSj0CojHPh0Z8hh^OZ0K3LX-s)eUtk0{MoiNyzoihg z!GEi&x}jP?X&BXYxHi5mT{#zxcsl@r!$fz!fB0Y5^}h_~qS-s~PP%;!3O@p!ERf85 z(9CuqT6r6<_4a3v*%v2n1NGo*mW~6V4@qS(q4a~Oq5Y5_jT_yT{>?hlTgl*uMUYBN zrxXTe5}0Dd@Mvmne_@7BTSZDzzPSC zS~Q_X?ek|AggmZ#R#|4da-=L4DDitNm5BN9cMbsB)jR&@Qd(NdZ`uXQ41^xsKTJTf zQ@cez7#qg{SOZx#?WJRVxpIBgyCgX9tghFSN?S7@-uG9T_3}B)^T|!7DAD;aJEE^Y z%HV(N$yLJ1e>+-r^raJ}=p~$GWwqA(Yp&sOgvo2Z~zY zz{n-M`Chi{GiLf8{ti#i-tSykgTuVajN4oKYx4U0`%^77ak(xbDk_+WegPaRVVu28 zH$1c#w9A`jT!;pRMGNQK2lIuq$c2FCF52?@_)u@1NZv*|A28u>%dvU7DU_~I#uuP^MA-os8Dgww67&tKS3}`B zI5QuWK}PUybOhO(`cJj`%g$Se`6}?^?TKn@r;dCt$hrN2H85a^C2lO*8c}awXQLVI z=+yb_ndv$};m$k3SO~(`AVx(vVx8^ub@J7#JOH92p5bN3y}42x{mEnj`e`RTaM^n+ zmLvH_J_*h^X;X^Tm8e7;)g6LvaeuxcdbjMxlcIC-n4D9%a{FkC|1Do897Q3Ofl0Ug zz4*cVG&EvTJ>;2f>48l}lc_k^PATk1FUW4whQdy@jzEJg23mny+^S zH?4ogb{S$x%e9`!mc2p^k_v$4mX+a**>01gY9X@RhaJy>{={|bOUJ9$*5 zLU(9_(=G6KZXZ<@twHN{NQD>JaYx6jBpZw9K~s>7w+hdGLw13FDQ16$6IW;xrQE=TmM%~6FN z7r$RrfB`06oa2vZMnhcnxu2xk*xB{y=zW?sA0Pi=ogF29+SFSuhX0!ZHJ9cJQRNv< z<)l19|6kA6h3TIJhWo1=cns|MaY?lG9|gZT3Sz(ql~WTK(Wndyw6*>H_m*J{{OkAc zZUgJAFq7v)J;a;NwH`3bk3)D@dBYK9hD%)|x$6+!kQ`EY#3Vsv|Ksd4%$FRr zEc}KP;O)<2IEp6l_g^PPc2Sx6pRD9^hVPFSU_j`~zL+4xeX94Yfpj})m|nJ3%?iIv!$w?C0$CE=$7$Jd-h=J^u#<)uUaA^lWDDRl#zc%&|1Dgj zrz4Y(9mXw2ekb+w4UQj&C*q}`Ht(`tr!+j0h<-;-urB-9h&N{f2~flgEY9+zMD&`h z|6z(pNyvbN6vRtW0+199A)!2!%w_?HJ=;gX7R)KO-=;bNY0hijdXMZ7L( z5$jX+hhmf=={LB1ebCrA5c9M;NC>Cvy5%+sE9k+?8gsxTpoas=wV*Pvh{~nQ^xoqk zXK6ES9n=@wnSGw-Vyc0y0>N~JTv;1#nwU%1+EsU@z~Jb!v$N!1pa-^%bNe&Di4aA@ zMs8z57!keR&o^@yJ=nbK0IO=KThV34sj``5)b`j9+T1k%{{4GyO${NCtmF&~e$1LM z1q1kwnoGA#`N5~rX$5g%4y4z=&*Dl6NPqEuS5^)J{D#fp$?tkU7?X%~-90!Xi%2;s zl&i9h5YfWAJ(OH+3iBVac$Y09##LO1OZy51(F$(pC~besfFGN$?+K1PyNECxm##p| zMOBjdfMp6`iYB>SsX5c%gsU?ds9Ppf)JV0+LGryXT&e^6VjX)9v*LTN_~F_mAUM$g za+tSI4u?u1Bz>`umP?Yb!9WzWpXoME@K$2y!dn92cGrt{rMl%kZWEFbfXg==;s7Vz z;I#17zM`4c%p_1phvc=_X$(`GC=y$iJ7M?>x;OxT7fYTZ)C~Y`U-;ApY)P%vSpG@i z3Cb||4-@OV^q_XXJFJ`b?Icx~xldl*TvfxKlt5mt_|&3*s;6lXbye#=|L1pc7TUtj z*QD)SCIJS8?#pnrOcNbc_Dsz=36Qd@P}ebr%w_}bs{#R#3tsT;*Z#HlXsVZtQBQOb z=}8I=!D9LY^`NF;Vaq+X-_+zL4;eB3Q-#tN1dIxLIHCvADw}i`I0DQ6v~#%>;~QqA zXu@$QzB0&#gCfO|D?6Z^o@eS3Kb4trKz$87KWT!XP~VxkIST-OBZRXNk3gg@3Pucr zu`SN}x9JV!fLuUsE zAUP46`l#ww<%Jd3-Z!?bWw&ux30utqz`yprt|x69Aq9DFgnWwvF5244*)@n+F6!ET zLHX+TvagiTkCc!zpv*A@v7HlSiyKQ^0~5qy(nGupMuMdoJ@-dSIK_Oh-OH{v%C0bs zq)z;P~Uqq2* z7}+Yi3cv%59sYdB|7BbG>C~%J)hF`3AZ0%g(!v%O7r$HBE-l-GV+BKlw9 z-@rw{-AaUs)f>hM#F#>=B=Gdk6gOSWm++rYDR@*FKdVIuHd@7WK?!B1Hq|iMBA{0z2X23*+=1xQv z4j$l}&6gJS9VNG?c_ov?oawdd1LRtQ+v9~0fOsDWvCEHFRT+NqI$5koq|1v{zcUtp zPr%eR06zO};qDt(Mc3!FPgXv0p#s1ey{-W1KPW676;xLfjHK~4J~ckiRtAOnsn_)26Ms%r&-s0*ECx7I+)X+kUNOD0J=f+VeOP zU;xuUFc(ILQ@D8UC=&mhm!5bZcu919FwJ&y%S-c1iH~x2=d6x87==RW6lcHm(k5vZ zBG}K=J;_J$^s9&BnREs@VAZuZr|QspG~uAKQ@GxC_MVOT*>;QJ)RJ#N3=1cxiKWfK zY)u*zLm|MrwJj|!x}6|*{#Wh=W_eXKimYj1C!NT$3o>FVC8DJZ^0>67)c|l`$Tzke z|EWu%$ethoE6`^_AFL{xHnF~^rHS)V`5`(S|C%Xu(uqw4FVu~P!S5ipjgFCu7yi%S z5Qfj;?dNSwU7|ifMrZzoocY0YCy&yvKNCGS0*#wIFmUzf=mb1la~~apr3{u#0U1k| z!;b2*d3rcXkAZ-?RurnurgqjtUJRt@8QUJLt=hg+E+7r2n7qRN`ST}YIxeE2gyV?p zdVdlbh{4=hr*m<6)IrolQS#>p#W{wD(|K~}Pe3Mfw^6*UIj*1Mz7sqARp8PT27U6p zjL0E&Q!_E9uK228g0L{K zh~l$bnaM`-v-VI9OLKO^HxW_nNXR%9#Of8SB|o=eYqhgjzQ+lBN*c8Ml-Ajm^&iTV zyV%2a@+7bj)c{Ka($^B$puqc$i0>)#2>Rf6>o9V~P$SpiP`2d(D$A^2r8Q`Dq5&R{ z`$i1Fpc+O-%dD`5vpNkIW)cJ{_eq>5<2Op`Hp1k^8&01wAB`(3TqysNg$g6K$sRP49Kc zyxCh!_m5fQe!Lo3qkj1}&@PO4W)3@=xvVbX<^?F=@pcTF*mAJ`rJ2wB4?d!P<-fqy zd`8s>vwB6q6%b1KaxWz7)RyHi_UKXR2Lbrq{Uv9Fw{sa5CEC@v<{y$J?x}ysq(b;| zTyrxud^WV4jEeGHjn5|G&ls00k5U)DfCv}z5R~;7c9D{x-7r+7{0|9giLtZcNk*lG zE^pHV^T(S;%cn6no3VQ;S+u{>YwM|e3yvqWjIBoL_g%<7AA39j6rTDx?-NtKbTZHh ze|A>FLuNW=`Y4?G1@vW9i7vqMcfi?d`_gFha%Cs1Wk>4y@or+@lulgK_AfoP*VBb< zV?19tiY+WY0y=G8s zFWcT=%B$Jve35vZT-?)P)tQR52Au;WwhN@7su>u-yF6`JZ?8^EbW$HJOD|3A5jWMT zi_Vy+7xOURLt9%~sHao0f`3jWMR5-^?bNyEjq zTru{B>`{ZLL66G1uj^RG#Sq=o=DduToyT7`l2my;Y!v^Id;T*Zw{c~8khPFpy?yM; z=TkY&K^-J=-BT4h&5ClXpky74cV^c=RuxmALZ2CRl1yrJq*Vd_dX|-X zu}9l#X`0#p43ptDIPH$0t=XxfE`E664K!eVRJ z+rP}jc~AuwN#!|^uzY{D^eh@-+S=z5P#tO*JwO*Z`$*@z`y=&n=j#=36R2@5(9dvB z-Nt*_$y)XIZySU7n=u^JrdT`g2pb3@r8PI?Vd{i*p z$7pPxtqcn9Y!{D@E^&aP!+rz@n-L`FXlN4X5$nmjD)7|45|}$Iowxfh{iDq1G&(Fvq2a{-fz4%*NWZ^>x!ar`fknS>OAE zU@(||hkvtNOw3^9=WXZ-TzOY7@bCuUsXRm_E4gS||1Om|=7|Uhg3r%g zHaxc}MUhOE@~_}=NgxO4gY|~+$x{7?*>i{(1OjOjcPnyf9j$pKk&+d>J(V+GSHxiwzzVUmV7M;MBd$DDhk`vO6_v+o6Hn&`5K(A60t(Sv!^@ z8H!zS4<54V)1o;KKeeit*7R+s&GCo+mh*IB+*ALatVOCn#ulnICW^F~@rjvEs!bnZ~rD3B+B3IYsTo(R4nzKz6*Ig6NTJ!@aEI`K)$EK}0vij-le(y+; zsl-Tc=M$tq0WJ-Rm<**k?ecrQ{xg&T{>8cNftB|A`Ab7JUx1u_|4y7I{5lJ-$+dU$ zsGy*LX44~tvvw9P5C@vqw6ZGdpL;zlo;5}oC!`CkRLt4g*}ug+tgv5O5j0Y{xAnV? z+vxYL4E>dvckNlmizB^8vbCpP4YkL&IeNAqr5c_2DsSL-LMi{ybBDzrOO_r-!=J`1 z$`&7})cy$kI4@%3Q?RZnxq`sL$j%Fg@t}$Mo!?J_z8-<(8*7s6Hz+~k!((Ih_luq< ziyY)p{)IU3yUsgfRK`RT_heT6Dwn)%Cad_g@9UG9+}ym}Ppw~1rQP;ZUR%qh;5~Vw z20J|jrpV__+4*Txu2#+*tRI}x!URqhSRp}JgX;5PZtAOMTgidC&ww*c2caTVX#VUqy zd??=hFU_Q3^X1{@>1o{kcvjewK}%KT$3`c-M)9u>(e|KYW+9>3P5YyN<=8J?yqJY^ zi#RW$OiuS3HaJAR_BdoGPJE@v!otAI`{JzaxnbMSh?A?g^r6{ix1u-M*O|HL$Vrj_ zziMA2gA)Epd-MhwKDv?yF_vmHkvemh>V#bNeESc1S4HvRMzL0UEaDMvyzeDr-vf8= zqNC(ced2U;bL;I~^Lr650sS?5r zo~gLv>1XPA&Qrg1SzSF@aY5lejm|9(JaQ?1xX7;@<4em)u zL{x1$$$0{m?;o2Iy z2oIpyx7k{1E`~t-JI#=j6k`c1f?dSL6p;hD`PY>|r*gpzA(|Ak{`FV zRNvF*=H}9)prLV6|K|~dhKxu1nt6GA61zFw{$HDz@be`$qMk2@IWyFo?K3NpI{C0m zk4Cr$~`@&Iq|^7treNQ1FlgaaUV!<#jvc^&@q`5(t{ofHKnC5!HS1w8&umg4(+?|Ue86EFhO#>wVZxxLf4sTv5Z64>XB zf_SMK?+ki^IS{+e-RVj?pBYk3Z7o+vgVh+NjUeUr_O`xzJQ^+~J4vY}E^|Lh(f%of;kOm9kbye$?5^T^yeDkx%`(3s+MuO#2cqSXsh-v1OlpS4*B zccBO21?{a3y-6GjeU+{OqF=L=%|`nZOu(Y!Ty$R9s$Sen^j<28<3mx`3aU7-h=@pE ze?MYciR_7lgajucC`&!UpwvOHie0v(3#;V>2@CUmz@^5Oa#2N4Ypm3jcog~aP+ON^pT2Ckn zpzH6K6dFfGZDIzgM<6Yiv7JMYD#dTkh&e)1KqQyIHzSvLe+gbe+x>1a8eP91LbSpn zA}n?eXZ4{MgOTn*n$#bG_&2!xLb@-UF3{y+oDPK0J=+l8`5bKGf1!XwCH(F*;ZK~y z>>#HU61^e$0EJPL3*vo8@{VzLcXtVl!uYLPe+rjg=;Hp3@AbiwUS8zP&Z^&8|BBaI z?0>4M2;I0pNVZeP|6dC*I$YzW>`8);9|TZ+!mWY$H2047+7dd7s6T&{^ArjV%SjPh zy|jME{5qYA3BIl$fCF}s=6m7gbk@j*$#8H`deE zGpXZ5zELM^KhHB#C%48BU3hJ3LkN6&B+i3m3B=HVC%~Z*#-S2%>?s<{;QuniKI?o#oHu>oGPmM|hS|!d&P_Z#ekhPwXQ4B)RyR@=H_U+qm<`d1XTWH2UJ0u9D zINx^T>b=PM=f|_2S-4@HO|U5$HBzO@yLULS=T$%Ny%n!&$0J?;=bO3t8lT$Ua+1Gx z^^vi${axsEeF=GT1jNLaX9?8mz+UF(=ez8VQbaFp@9u6sJzUw$SLJ;PPMqGf4bnvv zosn|u(;p?kPYR2R5kmz5l3Avf?ZwvWc|zzk&Bd=7tW2GgkwHc1cc*B6wh9U=P>NgM z%Yh8sIxd|L(m`hQZ)daH+S>j%FQ9dgc3IjK)YH>*Oq0|b9Uo8j78I1x>O!4ojA0Px z0n$DmPT@)yb!CjB7Bj!HhCJ+~m8gL5Xe8K|i*iCw)GOE<)TsIzY(rFd+fhQggDjNd z&F3mUiTYl%t7nS_W0SqiZEBh?;zvb6$pvEJ{@-dQ-`m45z?`y0cxwSa(yzBm(a2zm zcwxcb2u?yNLt(c85JI|y&#NzPZYP&db#;Q&QvT!s6{#P&8p#s()AinF;$z34dF`&j z&bYU;yBpqcvY6-C>Pce4#l_Y8aJ841nHjVFp*X?!f^lkU>Riu|vZl5sAuTN;Kc8{K z`oR4)N<=6X{oPzF zE917bvSJh!)$jiEhvK&{is{a9ihiRL4PsO=;KTd-Hq6Y-x$kqN&Mz-(RB(2rp07WQ zW{OQ0O_W9L5Bsqkr&UdFykrx5p3wC6_NHfKL?R#{xWx-Y>GL>bei9GdUw^Z_lfPu2 zl{g&(Qht76(B#4Zdt8I-`Uf6Nu{>Q2(jjWsLgfZMpO zY2a+F$rV}L^SCRHMjW4z@N0JnIui$nO5(-{04%lu<}7dUV=5xgaGah``d8@l<1Oa< z5P)xD0aO!9L4I3;u*H+a$?dWa6P1y{uX>hBLPr-VQ_`=YpDR<~)vGF(4OxJVqgS?F zVvT1@a($%!{P}abpe-R57M5jeG4*l4>YWqjEF3Xu9zrj#&MqNA?Qu9mVCS~cgW6iD z#yD8efJ*L(tr1R&ywU1dtr)!;b76UQeL11KrD(g`1 zok_q=ijFJ$e>pVb)7RJTM$P^PzhXN(r5ArVnla@s9nJhscFC|9j02*kn80inYL(ZS zF$aUr5hA8-#ftoGbT;un??(SKI$GD{sH>a$cQUhKs@csNc=$o53HT@3n@#|EH$TTK z%j~`U6uOj2Qbwk#ClE=N%rA=D!vK-MZa3!D9zF1X`%RV1mdUkb2{qU9(=%WL zhzVVQJII|Z)=zc>_#p<60Pdjxm`mQIO+rQnLUh#}FyL?f470yK6uSVcsB4(!uVGDA znL4nzyFROw5tJVv0bEB@Q&TkXsj1(w;^~I$t*YuthjMz^o}dRve!-5#UR@mzA$vZS zA%YCL_LW>-S<%$iE-c}J?0$=mMu;z!nRKqc6qONqZSqSESOEboZH0!s{CFu4 zd1Ntzx=p(l1g!tIDPW@(Fk0ug2!U>gjibMPb9sh(%zDChw6wI2b-ybQLt`okZv}}G z@6|yvG&-_Fz72-RxhK9J0@Z?aXk?_f$#o0Sj4{9M1A$#coL|)Ne5?zKkB^rnAP^G)j>G8s zXzD*zECso7We z4Eg?O?(FQCE!Np0DjkZ{GFT7#3tkV#(UMe5{4LSpB4uW%(fZKM0Rkzjsp$YfK=B(J z=*y^@lDs@3+}*7~ApLVdxO6?7;a~;~f`tWjZz5-KXCw^>Ucpo%PWsR9lsxax%@9qR zf6s^-}IRmtK1&GN@EZX6+FNPfbq|n-Iy5zo};ed2ODY!~%4GrAF_c z<*SX&{%B=&^?`+2YgZtQ7`1s5K%Q^XRAVUt++hY-{Ct%eGNL80u#mZ_xp}2w%mnfr z=G^gshNzD;G^7-y#HAFp2{OmT)TuOXXI4~~-vQt&J|Q)CT?gCqp$;k&qxEJLri|LK zcsYQo*DG32ORn47+9VJWTGZ!~IVm~$0?6wnmwnP=JKNg`%>n&dD_n&5E)J#OeyTk} z$p5|614;xQP0QzBaKKuA5m9ow|IA5CBYW+BY~yh(k1|tfV!{sov-uKKlW>TKtd*3L zLkEDjgoFf*i~8eZ2Zx0kR4%;=_ruxRgM$M^6hg$Z7zzP2wV#6B|H?mB13R+x@_gFQ z7mIF(iipS}trjaXQ?pkwHnd?Q_4{{3Tz&KA4VI$9+b;{fK@L28eE10Y#{Oustk+=n zJKzZr#V4dDyq_89?%+g2Y@D2Lo78{}A_BEArBQ-^sRLOQQK*GWB|I-qtdo=ks(Rv0 z^rsZEV_pv5nW4!5(%3H{gC_rnR{)g)K=nO1EG+C6PpMu4g$VTG#S8bt8I?SnNTc^F zEgoS@Mrx`bPV9MqN2tbv$Z^mRvosK$Q9w!xyK88re2;kr=dmzf-`L0lVn;=Bpk<*; z2F^Z!gEM=Zad7i;Y&P8)boeE9E;U9pQga)mUcPU*GZ(Hfk7^O zOo9g}6zh|Tkx`ij^Y%MxVX*hziq}OynaR93j>2sDM=~a*IC+3oX_o5p0KR7d#Nm4& z+YrC^E&P7DgCxWF`5t#%ZN~_4K&mN7pudl9u`O|eTzr}#* zRG|uhC4ZSukbMG?6BCDq<5F=F;Uq?1-QJQqebCSV(RKF)&vfesl>K9UpEwC@$~oX2 zOTbcCSXs^gl^LYGhPW{>Fx;=fVa59WlJDQYk4%As%`GfQqQW9U(%lL{AX7nd;>gg< zs6v!3%U1?Z42=iV#o{+ly4u>rR^ypIgINBjr>E%x)_8|=mArm9TD$P2dS_lgYI){Q zDk@R!a`aIES9?8!(N*!Om6+zXK?LBRz(;&EdONqYL3UdNhP|J6Q2Ub*qV}na>M#6hlN*y&a2BMkIFyb z&PxsDIup$KOI&TPa!s*mX=#Wg;~I&AsJ#|0za;};iIDw2)4oW8#&)O8z9<`>-qtl% zwV$~_O4D6qqo@C07r9~_HHVL~F`GV|4)ogVfbqaUzL1)ub7b3)nh`wT>{dLMee;JY z-gKenGX^$x?LO2x+h^d=nw6E+Fd+U5;*|N!kPy25h!Q%Fqkr{H<&BLZNFiTCv-b`T zI8+Jc<>ec8@c#=E5D*}NgabcAyc~Tz>oocGDedJad}3l|cWrB(yH&r+?FJ!CJiLMX zi!J`ZC-1`l{xe(a2vAm0=|&`>tgP6YnpebRWWBw;$Qv6Q%F48#26Kc<`!mHLmG#G=V@0pm0*ad|CRbNJv=3z>zUA^3Tv4l26wJ zq@-4w=E(s00X{p=uhC&r)=ppEl3{X9b$$1%u@kcUJ0ldEBBglRZ!4|9w&_=!hw1$1 z04#x}QRf5Z%Jk4skE6L^y%qpzUwKJNudOm95PgZ%>Ow}tk;xEtv}{gmhCM%8JY4RO zPTbA){ggbX5%;V+T=??ZZ8ydxO$=g7ziQOoMec%}U|Idu_! z0>dYh7M5HQf=NY z;4}cnfq{X6xUQS5G?5mxn^C?^FMJ8YAzc1aTnJ#gWqfX7bH_;r#+n!#n(sg32-#lk z@iBpP09pyKGI|Dv?$tbcNy$%I;zF|%(g0d=$w*Hp2T)IbNd>kO^7T4#klfZw5IUm= zicG=eAL=M~4no2p-DR+hB~iWy=$g~l?akqT_ryV(E;l6w%|P(oMuer`yL)axcGwVp z2a0qk(BK1~-`?tu*<=4EQv#K_Z}wphIpU&)#3btPUH+#kJLl);R{$g~Aa5j!O`=Y$ z8fB`Iy37lK?99}V=soU&2w$3XaJvVDgyW`QhDJIuWPxU^L_W9-&((Q zFN;}w&78CM+3~*b^E`W>vk%@VbTd>xAFb|MoJ5T+HZCXS=Z?B`!$^RI>uPdFhUjE_ z2nlo-OM-osYtSp4pCnZe6h3in-29;pSG|N_L*zq2zW`DxA~eFJ#X)keyy+}{^5;)O z;_k4X^9z9Q+6mvKLEqlnIWXp+sh~$AZeoXq3})w~7Kil8%{dY zOM!fl9U>Ysnf)_XX;e)~L6Ia#vzkj1ZL{ntt>i%j#c8Xh;>{3aZRd%x7wUMdmnYLr zX`p__l+93LL;5RR9`~iBq;M`n2KUp4G`Fas2kfG;S0x{aNlFf_%OexIc+5Ks0vde) zgH|%M*A$Rk+}LnC7-fi$=7uOZ^l@{nj{!l86{J}y%gfKHsxm0hzC8kQRHPO&Gb@g+ zdP*-BSE5gg&yAcIn1V~&oFhpS)d%^Q8_pFVxs+30og z>CqAWwO`#%x_}VEFb(MU=D|V8&q3N}EiJ^P&TcuKjdN^)Uw z@$0b&5{bkoAmEggq_ckVJL^u4io;~xmDiFk;e7r51#J|ncZ>-FRX;+pB5nB1q;L5> zQ^A*9%oa*Iu2%6Y{n^17V-*9Hu69V zC(=^PGBs`;ZyJ3c0iltbmsfO*5tEZ+13dFbwUcq%D7>@nmntiGPqpCPsR#fOa zt5#7_p=)j~GL#3HCm6gMo8oBzi z8A5`B@H_0>XPOayhqEEX6cj4pa*sCr4`$Z`{u*3i%Tp=5rqwV>5vaQpo`kMvauUDQK3BG5GPz`p^M%4{Fh(d(FkhO!J#*=oenIjI`%6m4)9dK~;2Ei*1hT z{Dj+Q>m|4B^mJBuGY+*s!(<1O{x^)uO@pttC>r)GY`+p#9CZR@mxUouS8~BQ1v25h zd6TJE{``~j?Ih6WhrNMk&ud&J#yPHbFKC!J8-?gY!(Y{OI?@lORcl}1PmVUp1LiWn z-FiAA?XxSur=1?UzcxNQz42N2lSLbIXc>aIRo0O;m}87)13AFTt&=d^VHEfCq1 z$%Bo6>~jZKSc|mEmLte!#)Ts(kf>B)Dlf6tU5ur}XyaI`h54Ygz#{@AXo>&H{+LW6i(|DZJ`oW& z==$vJY~qU-m92ECU^^H`APRmh5g{$7!=DF!ef277PUQ}@hNgzD_q}{GO&-f}5odb8A94A;dkY?r4ihrz8XyGRX$tTkiiuQ$Cv=AEuE4UdFeSPH z9by_-ONPI0viPx}fH+xCKkoJG@W)F%ur9CjTcNosA+fe!ADfWy+_AhV`jxPe)yT;P zcbPta!xzpQg6Hc)?l^(FG2BpY-D*$>^O4l7xRas%yLa#o@~GE0C_(Vvryw#dWdJl# z8J;D+%18LYWt?Ludgqf*BYVI0YmRz#TAuy&ds};8aMN!FoGw}`cXh#20w5kbvrK?c zdBvfRgZz>-o!h6qq@vMj6PU37Yv;_&g}}}uQ$X`^IK}K$w!Z{>UK$!2@9nm1MDBiK zGUiQ>70BP*bgznzQZ_edHSK7}0diEfWrFMIAYz-J>$@UG(fl1F-qqE0({Gt7niUnf zuwa)sZYAH;Z`BM>R(I!Vdk5}2PY#!7&h;9AvcvoKvN<`?*9a89&eLJZglYUj@@iZn zJp?um8AVO7-%3OpzGRKA_J2|)^Ze6?$dJZ(g_kC=L=k>}eeE`g@8W)6hyiD{-FPGS zqtf>Vya*tB%p)WrOs1}`4lla?OqCksj2C!(1fmPK?rh1xisVfKg>YdyEhya)zu8LM z>WDvcruF0Fwe9HX?ryTR@mjz}<)|1Li7@=~9v{Syj3J2_RR+20q0;f^%!7(H2-XeOBy`t+c_L!M#w0(ycCHyX3Mvu|6%nq9Wn;JwIu*NCCM_dEe{0kvC0SZPc0rk6e7IH z8tOQhH~WGAjqU=ohm44KJ2BLAo*>Q#i4_3~;Ef)cp0><(aGtLfHQQ{ja=d%CS9DJdz_$ndWOWnhgznE4S-H9RfMK-(*xPEE z{8%Ivlf%~$%|`3m+M1w7Hwx7W2)33**AvcodR(yz zD9Cx=Ty>=24!4%y3NO4!l(x4DZ%$8o*`P7Ot?7ucD<5sW_xf6q*z27TtBX*hMXw;l zkGlrt3gq+)Otw!3i$vC1K573H*A9X74*t_z09HDJhU9Y;!1f#Pg*C&kje+eJB>bpvP_8ntMBxzS{Nn#|9~ zNBBI1{8mUb8@)e;Ap;9bc$a?OOy_Ltp~Fwv-28kqf1T$o*sb{vZh3j6dXB0YGM5~v zX70&6d#y-NaHTEY%-dYg_=lwz-v0?aZ~Yc{ZZ@#;GHKvpb!bFH>w9W$DmJ##ACH%N z#;aT+j>s5hDu;{Vw*VB6Vwf%xYGqs{sD3zHaiXSRZOs{tlouPy1N5=TqEGF28$MR# zeuQ2b7n+|RV1`9=_H!b7-G z+3)aWXTGCVzr4f$V2WQqajwQ?O6b&gB!xs!{mV5-Oyeuc!3?>smryaF2k&uW^2vc3 z#1XouFGs##yhlp|2L!KEA%>}364dEN;0Mnxn$xOBIfe&UnaB@Ee|#V&p-{;`^iJiX z+gUAdBCT~S#buD&v6Ue>lvj9ISBqa#W4S{p=Pl>kX90H&pR~;B=MWR4$cc~tu_Z$c z97*j@$iVn?8@xmGjwGGMI1o_C5AA|6rrRarrVO zWen3ZN4yid$j1Zl3|(!*qVSb7-}0 zZnXZG3&4)s_mpQS{}aKg4c`lJpPT}T5;edXbAFU2s#9urTMu7GaV2C#QE%b<5_)V zoMDYk8|m!z{{DW7Kt=vBMp}rGfrTb0f>F%G&)>+1;p)I3H4Q_ank^L|IiQTJLb9Iws*~0Dg&Yx_2PPiguH5){LbAhE;U!U&X zd5+OmWvp0z`yG5l##phF|M26gXUhI!S^?06 z3%-eoi8F=yZ*X~;0Tn_F30*HR(@$0FGV=5Jk9XHyhAYD$&?V)sUP#aFEPih-OyCcm zNY&yiG5ttGk|AR|Tx2M2`ZPT4BNhvNlC~%1(@G}>mOr+x+0y~20BqRJ%?e!y0zfqZ zD03ag;eyE+)HJZP^ zl8*6z=m{F0tiFt45K(v_yol+l2I6-%s`h8aL!oX!Kj23Cwv*3})(;ICd`p!x8+eTL zoD<>9(}1JI+Mq-FAHG-;YE6i@%nbFr>CNwU_v3>y|{=v$-aiH%t0zUj=fpE8<6Om<->1GTJ=+QAS2#p`D z?)k{>{HaXYZ@={CRGk3?$}cHbY)eoE{4z3^Y%_f+4yG~4fZ|1y>2Ke($dS?p9!p3z zw(mWec5Q-?A_yJuGl}O6jt2Auekl^VpzZ@n1)K%HvK2H`>QB<#uldi6sw13|z}$bm2h z&|uORLbuZw%PAlr&@7!=^!Hx9CrPr{z4%`iKtC%+zF?DCNHHP8It!Jc*~r!3T!rwxK;+EQLqDwzm|i8mh>fN44Y`F5eF7B(spPZ_73xznGdqsnghi5ZzE(>_ z0^f%jlCieVJ2|n5iFSK91t$;=+JE1U?A(G)j2JSU`!)X@^Pop5r$8W0-mG|dZav`d zd-pAy^)>zmUx!XAS$S(bPw9+%Z;*6!FVrtLIVV3zaI(TvsNF0zBs&k-kmKRu`SB+e zRJJ{x64m_U(L5mSF8k(hC)BmHzNEP|+V6Tfj&yV$3XB`+EyklZQoYPNy z5u()5@m|>@rXabMvFSckZi%$Ew%(^mRl5#wxasfyC6rk;>B`EA(G=(d+s&C4CTYLA zDGXbzv^au;OHT^;8eosM^X*YLeHMtx@rX!|jt@CL=pcdJMJ}1@^44fTCZs8$YFq#} zdhXR{Jx&&b%@%%N;-4Q^a?@o(0G%*z3TU)x#FocUD&lZ}21Ze^Ny9W~-D})N?f3m+ zR8k0nn!2kQ=suXc#JrJPVbRW>019S>k;WcKLXJQGK5?Id***DWKImO0{yJmf2q);i|{Go?r=ga z`0_ul`hQgDKM%cv!a_q0Ui_1&UHRX}<->`=i+?&Jm+3z%#PV;2SpL5rip|B(Dk$j0 z=je!cs;sPhCksMc=P%;jRH?v&w)ndecjf9NM_y(?b+89q%ChWa4R|oc_92 z;0b8t$-zeLbro=d7^=Ye|-OsBloFLgpxy zZ<+Gb1Km0o)N1!rM_eydIS_MQy9U3^uX_6qXnT+2`s7D7ZSCq^kK&MX#40VeI?1BW zbxOL#5qOZIN|fH&xrU!rScZ>?a7%_q)f43xqT8k}ZPL($Wlj zc|i}^0kL;})El@7sCSs843xzU0nT8u=O-nEFqL&gK-2yQY%eL;!jS=o&pJqHspB-q zE9}WsBAGbHk1hoHudkWN;bjf3Kr@e_R;7uNLVY6mseT}1`f}B2@rj5Y+kx>&4{xzj zxPPAyor#KS^gXZv+F4CgGr^4Z zcOe$&z|7YY9LHhm#%jN!%LK6=$q3o!X$V>{AfCT6&h2?!#V~*yknbYgwPAxYD4)A; zS){f^DUbjqrjcZ!usZa9gy4Y0-REikOIq)cc6(e3Q7X1w_RkTrL|L_ll-!^T(Z24L zoiS~0uS?gPToR!~O-P(hK?hnZ@ev1kZDsJSgk4AWr+LSJ^wU6(QBXZ%`RpL`I|BcL zm7oI1&qoNQLg=|g^?-@w$vB|M*7dmivD(`Mz@^IZE3TBbUqP5cpG9Mk|#mS_L`Ux*CD6 + + + + LPic + + + Attributes + 0x0000 + Data + AAAAAgAAAAAAAAAAAAQAAA== + ID + 5000 + Name + + + + STR# + + + Attributes + 0x0000 + Data + AAYPRW5nbGlzaCBkZWZhdWx0BUFncmVlCERpc2FncmVlBVByaW50B1NhdmUuLi56SWYgeW91IGFncmVlIHdpdGggdGhlIHRlcm1zIG9mIHRoaXMgbGljZW5zZSwgY2xpY2sgIkFncmVlIiB0byBhY2Nlc3MgdGhlIHNvZnR3YXJlLiAgSWYgeW91IGRvIG5vdCBhZ3JlZSwgcHJlc3MgIkRpc2FncmVlLiI= + ID + 5000 + Name + English buttons + + + Attributes + 0x0000 + Data + AAYHRGV1dHNjaAtBa3plcHRpZXJlbghBYmxlaG5lbgdEcnVja2VuClNpY2hlcm4uLi7nS2xpY2tlbiBTaWUgaW4g0kFremVwdGllcmVu0ywgd2VubiBTaWUgbWl0IGRlbiBCZXN0aW1tdW5nZW4gZGVzIFNvZnR3YXJlLUxpemVuenZlcnRyYWdzIGVpbnZlcnN0YW5kZW4gc2luZC4gRmFsbHMgbmljaHQsIGJpdHRlINJBYmxlaG5lbtMgYW5rbGlja2VuLiBTaWUga5pubmVuIGRpZSBTb2Z0d2FyZSBudXIgaW5zdGFsbGllcmVuLCB3ZW5uIFNpZSDSQWt6ZXB0aWVyZW7TIGFuZ2VrbGlja3QgaGFiZW4u + ID + 5001 + Name + German + + + Attributes + 0x0000 + Data + AAYHRW5nbGlzaAVBZ3JlZQhEaXNhZ3JlZQVQcmludAdTYXZlLi4ue0lmIHlvdSBhZ3JlZSB3aXRoIHRoZSB0ZXJtcyBvZiB0aGlzIGxpY2Vuc2UsIHByZXNzICJBZ3JlZSIgdG8gaW5zdGFsbCB0aGUgc29mdHdhcmUuICBJZiB5b3UgZG8gbm90IGFncmVlLCBwcmVzcyAiRGlzYWdyZWUiLg== + ID + 5002 + Name + English + + + Attributes + 0x0000 + Data + AAYHRXNwYZZvbAdBY2VwdGFyCk5vIGFjZXB0YXIISW1wcmltaXIKR3VhcmRhci4uLsBTaSBlc3SHIGRlIGFjdWVyZG8gY29uIGxvcyB0jnJtaW5vcyBkZSBlc3RhIGxpY2VuY2lhLCBwdWxzZSAiQWNlcHRhciIgcGFyYSBpbnN0YWxhciBlbCBzb2Z0d2FyZS4gRW4gZWwgc3VwdWVzdG8gZGUgcXVlIG5vIGVzdI4gZGUgYWN1ZXJkbyBjb24gbG9zIHSOcm1pbm9zIGRlIGVzdGEgbGljZW5jaWEsIHB1bHNlICJObyBhY2VwdGFyLiI= + ID + 5003 + Name + Spanish + + + Attributes + 0x0000 + Data + AAYIRnJhbo1haXMIQWNjZXB0ZXIHUmVmdXNlcghJbXByaW1lcg5FbnJlZ2lzdHJlci4uLrpTaSB2b3VzIGFjY2VwdGV6IGxlcyB0ZXJtZXMgZGUgbGEgcHKOc2VudGUgbGljZW5jZSwgY2xpcXVleiBzdXIgIkFjY2VwdGVyIiBhZmluIGQnaW5zdGFsbGVyIGxlIGxvZ2ljaWVsLiBTaSB2b3VzIG4nkHRlcyBwYXMgZCdhY2NvcmQgYXZlYyBsZXMgdGVybWVzIGRlIGxhIGxpY2VuY2UsIGNsaXF1ZXogc3VyICJSZWZ1c2VyIi4= + ID + 5004 + Name + French + + + Attributes + 0x0000 + Data + AAYISXRhbGlhbm8HQWNjZXR0bwdSaWZpdXRvBlN0YW1wYQtSZWdpc3RyYS4uLn9TZSBhY2NldHRpIGxlIGNvbmRpemlvbmkgZGkgcXVlc3RhIGxpY2VuemEsIGZhaSBjbGljIHN1ICJBY2NldHRvIiBwZXIgaW5zdGFsbGFyZSBpbCBzb2Z0d2FyZS4gQWx0cmltZW50aSBmYWkgY2xpYyBzdSAiUmlmaXV0byIu + ID + 5005 + Name + Italian + + + Attributes + 0x0000 + Data + AAYISmFwYW5lc2UKk6+I04K1gtyCtwyTr4jTgrWC3IK5gvEIiPON/IK3gukHlduRti4uLrSWe4Ncg3SDZ4NFg0eDQY5nl3CLlpH4jF+W8YLMj/CMj4LJk6+I04KzguqC6Y/qjYeCyYLNgUGDXIN0g2eDRYNHg0GC8INDg5ODWINngVuDi4K3gumCvYLfgsmBdZOviNOCtYLcgreBdoLwiZ+CtYLEgq2CvoKzgqKBQoFAk6+I04KzguqCyIKij+qNh4LJgs2BQYF1k6+I04K1gtyCuYLxgXaC8ImfgrWCxIKtgr6Cs4KigUI= + ID + 5006 + Name + Japanese + + + Attributes + 0x0000 + Data + AAYKTmVkZXJsYW5kcwJKYQNOZWUFUHJpbnQJQmV3YWFyLi4upEluZGllbiB1IGFra29vcmQgZ2FhdCBtZXQgZGUgdm9vcndhYXJkZW4gdmFuIGRlemUgbGljZW50aWUsIGt1bnQgdSBvcCAnSmEnIGtsaWtrZW4gb20gZGUgcHJvZ3JhbW1hdHV1ciB0ZSBpbnN0YWxsZXJlbi4gSW5kaWVuIHUgbmlldCBha2tvb3JkIGdhYXQsIGtsaWt0IHUgb3AgJ05lZScu + ID + 5007 + Name + Dutch + + + Attributes + 0x0000 + Data + AAYGU3ZlbnNrCEdvZGuKbm5zBkF2YppqcwhTa3JpdiB1dAhTcGFyYS4uLpNPbSBEdSBnb2Rrim5uZXIgbGljZW5zdmlsbGtvcmVuIGtsaWNrYSBwjCAiR29ka4pubnMiIGaaciBhdHQgaW5zdGFsbGVyYSBwcm9ncmFtcHJvZHVrdGVuLiBPbSBEdSBpbnRlIGdvZGuKbm5lciBsaWNlbnN2aWxsa29yZW4sIGtsaWNrYSBwjCAiQXZimmpzIi4= + ID + 5008 + Name + Swedish + + + Attributes + 0x0000 + Data + AAYRUG9ydHVndZBzLCBCcmFzaWwJQ29uY29yZGFyCURpc2NvcmRhcghJbXByaW1pcglTYWx2YXIuLi6MU2UgZXN0hyBkZSBhY29yZG8gY29tIG9zIHRlcm1vcyBkZXN0YSBsaWNlbo1hLCBwcmVzc2lvbmUgIkNvbmNvcmRhciIgcGFyYSBpbnN0YWxhciBvIHNvZnR3YXJlLiBTZSBui28gZXN0hyBkZSBhY29yZG8sIHByZXNzaW9uZSAiRGlzY29yZGFyIi4= + ID + 5009 + Name + Brazilian Portuguese + + + Attributes + 0x0000 + Data + AAYSU2ltcGxpZmllZCBDaGluZXNlBM2s0uIGsrvNrNLiBLTy06EGtOa0oqGtVMjnufvE+s2s0uKxvtDtv8nQrdLptcTM9b/uo6zH67C0obDNrNLiobHAtLCy17C0y8jtvP6ho8jnufvE+rK7zazS4qOsx+uwtKGwsrvNrNLiobGhow== + ID + 5010 + Name + Simplified Chinese + + + Attributes + 0x0000 + Data + AAYTVHJhZGl0aW9uYWwgQ2hpbmVzZQSmULdOBqSjplC3TgSmQ6ZMBsB4pnOhS1CmcKpHsXqmULdOpbuzXKVpw9K4zKq6sfi02qFBvdCr9qGnplC3TqGopUimd7jLs27F6aFDpnCqR6SjplC3TqFBvdCr9qGnpKOmULdOoaihQw== + ID + 5011 + Name + Traditional Chinese + + + Attributes + 0x0000 + Data + AAYFRGFuc2sERW5pZwVVZW5pZwdVZHNrcml2CkFya2l2ZXIuLi6YSHZpcyBkdSBhY2NlcHRlcmVyIGJldGluZ2Vsc2VybmUgaSBsaWNlbnNhZnRhbGVuLCBza2FsIGR1IGtsaWtrZSBwjCDSRW5pZ9MgZm9yIGF0IGluc3RhbGxlcmUgc29mdHdhcmVuLiBLbGlrIHCMINJVZW5pZ9MgZm9yIGF0IGFubnVsbGVyZSBpbnN0YWxsZXJpbmdlbi4= + ID + 5012 + Name + Danish + + + Attributes + 0x0000 + Data + AAYFU3VvbWkISHl2imtzeW4KRW4gaHl2imtzeQdUdWxvc3RhCVRhbGxlbm5hyW9IeXaKa3N5IGxpc2Vuc3Npc29waW11a3NlbiBlaGRvdCBvc29pdHRhbWFsbGEg1Uh5doprc3nVLiBKb3MgZXQgaHl2imtzeSBzb3BpbXVrc2VuIGVodG9qYSwgb3NvaXRhINVFbiBoeXaKa3N51S4= + ID + 5013 + Name + Finnish + + + Attributes + 0x0000 + Data + AAYRRnJhbo1haXMgY2FuYWRpZW4IQWNjZXB0ZXIHUmVmdXNlcghJbXByaW1lcg5FbnJlZ2lzdHJlci4uLrpTaSB2b3VzIGFjY2VwdGV6IGxlcyB0ZXJtZXMgZGUgbGEgcHKOc2VudGUgbGljZW5jZSwgY2xpcXVleiBzdXIgIkFjY2VwdGVyIiBhZmluIGQnaW5zdGFsbGVyIGxlIGxvZ2ljaWVsLiBTaSB2b3VzIG4nkHRlcyBwYXMgZCdhY2NvcmQgYXZlYyBsZXMgdGVybWVzIGRlIGxhIGxpY2VuY2UsIGNsaXF1ZXogc3VyICJSZWZ1c2VyIi4= + ID + 5014 + Name + French Canadian + + + Attributes + 0x0000 + Data + AAYGS29yZWFuBLW/wMcJtb/AxyC+yMfUBsfBuLDGrgfA+sDlLi4ufrvnv+sgsOi+4LytwMcgs7u/67+hILW/wMfHz7jpLCAitb/AxyIgtNzD37imILStt68gvNLHwcauv/6+7rimILyzxKHHz73KvcO/wC4gtb/Ax8fPwfYgvsq0wrTZuOksICK1v8DHIL7Ix9QiILTcw9+4piC0qbijvcq9w7/ALg== + ID + 5015 + Name + Korean + + + Attributes + 0x0000 + Data + AAYFTm9yc2sERW5pZwlJa2tlIGVuaWcIU2tyaXYgdXQKQXJraXZlci4uLqNIdmlzIERlIGVyIGVuaWcgaSBiZXN0ZW1tZWxzZW5lIGkgZGVubmUgbGlzZW5zYXZ0YWxlbiwga2xpa2tlciBEZSBwjCAiRW5pZyIta25hcHBlbiBmb3IgjCBpbnN0YWxsZXJlIHByb2dyYW12YXJlbi4gSHZpcyBEZSBpa2tlIGVyIGVuaWcsIGtsaWtrZXIgRGUgcIwgIklra2UgZW5pZyIu + ID + 5016 + Name + Norwegian + + + TEXT + + + Attributes + 0x0000 + Data + APPLICATION_LICENSE_TEXT + ID + 5000 + Name + English SLA + + + TMPL + + + Attributes + 0x0000 + Data + E0RlZmF1bHQgTGFuZ3VhZ2UgSUREV1JEBUNvdW50T0NOVAQqKioqTFNUQwtzeXMgbGFuZyBJRERXUkQebG9jYWwgcmVzIElEIChvZmZzZXQgZnJvbSA1MDAwRFdSRBAyLWJ5dGUgbGFuZ3VhZ2U/RFdSRAQqKioqTFNURQ== + ID + 128 + Name + LPic + + + plst + + + Attributes + 0x0050 + Data + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + ID + 0 + Name + + + + styl + + + Attributes + 0x0000 + Data + AAMAAAAAAAwACQAUAAAAAAAAAAAAAAAAACcADAAJABQBAAAAAAAAAAAAAAAAKgAMAAkAFAAAAAAAAAAAAAA= + ID + 5000 + Name + English SLA + + + + diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/postinstall.template b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/postinstall.template new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/postinstall.template @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +chown root:wheel "INSTALL_LOCATION" +chmod a+rX "INSTALL_LOCATION" +chmod +r "APP_LOCATION/"*.jar + +exit 0 diff --git a/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/preinstall.template b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/preinstall.template new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/preinstall.template @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +if [ ! -d "INSTALL_LOCATION" ] +then + mkdir -p "INSTALL_LOCATION" +fi + +exit 0 diff --git a/src/jdk.incubator.jpackage/macosx/classes/module-info.java.extra b/src/jdk.incubator.jpackage/macosx/classes/module-info.java.extra new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/classes/module-info.java.extra @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +provides jdk.incubator.jpackage.internal.Bundler with + jdk.incubator.jpackage.internal.MacAppBundler, + jdk.incubator.jpackage.internal.MacAppStoreBundler, + jdk.incubator.jpackage.internal.MacDmgBundler, + jdk.incubator.jpackage.internal.MacPkgBundler; + diff --git a/src/jdk.incubator.jpackage/macosx/native/jpackageapplauncher/main.m b/src/jdk.incubator.jpackage/macosx/native/jpackageapplauncher/main.m new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/native/jpackageapplauncher/main.m @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#import +#include +#include + +typedef bool (*start_launcher)(int argc, char* argv[]); +typedef void (*stop_launcher)(); + +int main(int argc, char *argv[]) { +#if !__has_feature(objc_arc) + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; +#endif + + int result = 1; + + @try { + setlocale(LC_ALL, "en_US.utf8"); + + NSBundle *mainBundle = [NSBundle mainBundle]; + NSString *mainBundlePath = [mainBundle bundlePath]; + NSString *libraryName = [mainBundlePath stringByAppendingPathComponent:@"Contents/MacOS/libapplauncher.dylib"]; + + void* library = dlopen([libraryName UTF8String], RTLD_LAZY); + + if (library == NULL) { + NSLog(@"%@ not found.\n", libraryName); + } + + if (library != NULL) { + start_launcher start = + (start_launcher)dlsym(library, "start_launcher"); + stop_launcher stop = + (stop_launcher)dlsym(library, "stop_launcher"); + + if (start != NULL && stop != NULL) { + if (start(argc, argv) == true) { + result = 0; + stop(); + } + } else if (start == NULL) { + NSLog(@"start_launcher not found in %@.\n", libraryName); + } else { + NSLog(@"stop_launcher not found in %@.\n", libraryName); + } + dlclose(library); + } + } @catch (NSException *exception) { + NSLog(@"%@: %@", exception, [exception callStackSymbols]); + result = 1; + } + +#if !__has_feature(objc_arc) + [pool drain]; +#endif + + return result; +} diff --git a/src/jdk.incubator.jpackage/macosx/native/libapplauncher/MacPlatform.h b/src/jdk.incubator.jpackage/macosx/native/libapplauncher/MacPlatform.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/native/libapplauncher/MacPlatform.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef MACPLATFORM_H +#define MACPLATFORM_H + +#include "Platform.h" +#include "PosixPlatform.h" + +class MacPlatform : virtual public Platform, PosixPlatform { +private: + bool UsePListForConfigFile(); + +protected: + virtual TString getTmpDirString(); + +public: + MacPlatform(void); + virtual ~MacPlatform(void); + +public: + virtual void ShowMessage(TString title, TString description); + virtual void ShowMessage(TString description); + + virtual TCHAR* ConvertStringToFileSystemString( + TCHAR* Source, bool &release); + virtual TCHAR* ConvertFileSystemStringToString( + TCHAR* Source, bool &release); + + virtual TString GetPackageRootDirectory(); + virtual TString GetAppDataDirectory(); + virtual TString GetBundledJavaLibraryFileName(TString RuntimePath); + virtual TString GetAppName(); + + TString GetPackageAppDirectory(); + TString GetPackageLauncherDirectory(); + TString GetPackageRuntimeBinDirectory(); + + virtual ISectionalPropertyContainer* GetConfigFile(TString FileName); + virtual TString GetModuleFileName(); + + virtual bool IsMainThread(); + virtual TPlatformNumber GetMemorySize(); + + virtual std::map GetKeys(); +}; + + +#endif // MACPLATFORM_H diff --git a/src/jdk.incubator.jpackage/macosx/native/libapplauncher/MacPlatform.mm b/src/jdk.incubator.jpackage/macosx/native/libapplauncher/MacPlatform.mm new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/native/libapplauncher/MacPlatform.mm @@ -0,0 +1,505 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Platform.h" + +#include "MacPlatform.h" +#include "Helpers.h" +#include "Package.h" +#include "PropertyFile.h" +#include "IniFile.h" + +#include +#include +#include +#include +#include + +#import +#import + +#include +#include + +#ifdef __OBJC__ +#import +#endif //__OBJC__ + +#define MAC_JPACKAGE_TMP_DIR \ + "/Library/Application Support/Java/JPackage/tmp" + +NSString* StringToNSString(TString Value) { + NSString* result = [NSString stringWithCString : Value.c_str() + encoding : [NSString defaultCStringEncoding]]; + return result; +} + +FileSystemStringToString::FileSystemStringToString(const TCHAR* value) { + bool release = false; + PlatformString lvalue = PlatformString(value); + Platform& platform = Platform::GetInstance(); + TCHAR* buffer = platform.ConvertFileSystemStringToString(lvalue, release); + FData = buffer; + + if (buffer != NULL && release == true) { + delete[] buffer; + } +} + +FileSystemStringToString::operator TString() { + return FData; +} + +StringToFileSystemString::StringToFileSystemString(const TString &value) { + FRelease = false; + PlatformString lvalue = PlatformString(value); + Platform& platform = Platform::GetInstance(); + FData = platform.ConvertStringToFileSystemString(lvalue, FRelease); +} + +StringToFileSystemString::~StringToFileSystemString() { + if (FRelease == true) { + delete[] FData; + } +} + +StringToFileSystemString::operator TCHAR* () { + return FData; +} + +MacPlatform::MacPlatform(void) : Platform(), PosixPlatform() { +} + +MacPlatform::~MacPlatform(void) { +} + +TString MacPlatform::GetPackageAppDirectory() { + return FilePath::IncludeTrailingSeparator( + GetPackageRootDirectory()) + _T("app"); +} + +TString MacPlatform::GetPackageLauncherDirectory() { + return FilePath::IncludeTrailingSeparator( + GetPackageRootDirectory()) + _T("MacOS"); +} + +TString MacPlatform::GetPackageRuntimeBinDirectory() { + return FilePath::IncludeTrailingSeparator(GetPackageRootDirectory()) + + _T("runtime/Contents/Home/bin"); +} + +bool MacPlatform::UsePListForConfigFile() { + return FilePath::FileExists(GetConfigFileName()) == false; +} + +void MacPlatform::ShowMessage(TString Title, TString Description) { + NSString *ltitle = StringToNSString(Title); + NSString *ldescription = StringToNSString(Description); + + NSLog(@"%@:%@", ltitle, ldescription); +} + +void MacPlatform::ShowMessage(TString Description) { + TString appname = GetModuleFileName(); + appname = FilePath::ExtractFileName(appname); + ShowMessage(appname, Description); +} + +TString MacPlatform::getTmpDirString() { + return TString(MAC_JPACKAGE_TMP_DIR); +} + +TCHAR* MacPlatform::ConvertStringToFileSystemString(TCHAR* Source, + bool &release) { + TCHAR* result = NULL; + release = false; + CFStringRef StringRef = CFStringCreateWithCString(kCFAllocatorDefault, + Source, kCFStringEncodingUTF8); + + if (StringRef != NULL) { + @ try { + CFIndex length = + CFStringGetMaximumSizeOfFileSystemRepresentation(StringRef); + result = new char[length + 1]; + if (result != NULL) { + if (CFStringGetFileSystemRepresentation(StringRef, + result, length)) { + release = true; + } else { + delete[] result; + result = NULL; + } + } + } + @finally + { + CFRelease(StringRef); + } + } + + return result; +} + +TCHAR* MacPlatform::ConvertFileSystemStringToString(TCHAR* Source, + bool &release) { + TCHAR* result = NULL; + release = false; + CFStringRef StringRef = CFStringCreateWithFileSystemRepresentation( + kCFAllocatorDefault, Source); + + if (StringRef != NULL) { + @ try { + CFIndex length = CFStringGetLength(StringRef); + + if (length > 0) { + CFIndex maxSize = CFStringGetMaximumSizeForEncoding( + length, kCFStringEncodingUTF8); + + result = new char[maxSize + 1]; + if (result != NULL) { + if (CFStringGetCString(StringRef, result, maxSize, + kCFStringEncodingUTF8) == true) { + release = true; + } else { + delete[] result; + result = NULL; + } + } + } + } + @finally + { + CFRelease(StringRef); + } + } + + return result; +} + +TString MacPlatform::GetPackageRootDirectory() { + NSBundle *mainBundle = [NSBundle mainBundle]; + NSString *mainBundlePath = [mainBundle bundlePath]; + NSString *contentsPath = + [mainBundlePath stringByAppendingString : @"/Contents"]; + TString result = [contentsPath UTF8String]; + return result; +} + +TString MacPlatform::GetAppDataDirectory() { + TString result; + NSArray *paths = NSSearchPathForDirectoriesInDomains( + NSApplicationSupportDirectory, NSUserDomainMask, YES); + NSString *applicationSupportDirectory = [paths firstObject]; + result = [applicationSupportDirectory UTF8String]; + return result; +} + +TString MacPlatform::GetBundledJavaLibraryFileName(TString RuntimePath) { + TString result; + + // first try lib/, then lib/jli + result = FilePath::IncludeTrailingSeparator(RuntimePath) + + _T("Contents/Home/lib/libjli.dylib"); + + if (FilePath::FileExists(result) == false) { + result = FilePath::IncludeTrailingSeparator(RuntimePath) + + _T("Contents/Home/lib/jli/libjli.dylib"); + + if (FilePath::FileExists(result) == false) { + // cannot find + NSLog(@"Cannot find libjli.dysym!"); + result = _T(""); + } + } + + return result; +} + +TString MacPlatform::GetAppName() { + NSString *appName = [[NSProcessInfo processInfo] processName]; + TString result = [appName UTF8String]; + return result; +} + +void PosixProcess::Cleanup() { + if (FOutputHandle != 0) { + close(FOutputHandle); + FOutputHandle = 0; + } + + if (FInputHandle != 0) { + close(FInputHandle); + FInputHandle = 0; + } + + sigaction(SIGINT, &savintr, (struct sigaction *) 0); + sigaction(SIGQUIT, &savequit, (struct sigaction *) 0); + sigprocmask(SIG_SETMASK, &saveblock, (sigset_t *) 0); +} + +#define PIPE_READ 0 +#define PIPE_WRITE 1 + +bool PosixProcess::Execute(const TString Application, + const std::vector Arguments, bool AWait) { + bool result = false; + + if (FRunning == false) { + FRunning = true; + + int handles[2]; + + if (pipe(handles) == -1) { + return false; + } + + struct sigaction sa; + sa.sa_handler = SIG_IGN; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigemptyset(&savintr.sa_mask); + sigemptyset(&savequit.sa_mask); + sigaction(SIGINT, &sa, &savintr); + sigaction(SIGQUIT, &sa, &savequit); + sigaddset(&sa.sa_mask, SIGCHLD); + sigprocmask(SIG_BLOCK, &sa.sa_mask, &saveblock); + + FChildPID = fork(); + + // PID returned by vfork is 0 for the child process and the + // PID of the child process for the parent. + if (FChildPID == -1) { + // Error + TString message = PlatformString::Format( + _T("Error: Unable to create process %s"), + Application.data()); + throw Exception(message); + } else if (FChildPID == 0) { + Cleanup(); + TString command = Application; + + for (std::vector::const_iterator iterator = + Arguments.begin(); iterator != Arguments.end(); + iterator++) { + command += TString(_T(" ")) + *iterator; + } +#ifdef DEBUG + printf("%s\n", command.data()); +#endif // DEBUG + + dup2(handles[PIPE_READ], STDIN_FILENO); + dup2(handles[PIPE_WRITE], STDOUT_FILENO); + + close(handles[PIPE_READ]); + close(handles[PIPE_WRITE]); + + execl("/bin/sh", "sh", "-c", command.data(), (char *) 0); + + _exit(127); + } else { + FOutputHandle = handles[PIPE_READ]; + FInputHandle = handles[PIPE_WRITE]; + + if (AWait == true) { + ReadOutput(); + Wait(); + Cleanup(); + FRunning = false; + result = true; + } else { + result = true; + } + } + } + + return result; +} + +void AppendPListArrayToIniFile(NSDictionary *infoDictionary, + IniFile *result, TString Section) { + NSString *sectionKey = + [NSString stringWithUTF8String : PlatformString(Section).toMultibyte()]; + NSDictionary *array = [infoDictionary objectForKey : sectionKey]; + + for (id option in array) { + if ([option isKindOfClass : [NSString class]]) { + TString arg = [option UTF8String]; + + TString name; + TString value; + + if (Helpers::SplitOptionIntoNameValue(arg, name, value) == true) { + result->Append(Section, name, value); + } + } + } +} + +void AppendPListDictionaryToIniFile(NSDictionary *infoDictionary, + IniFile *result, TString Section, bool FollowSection = true) { + NSDictionary *dictionary = NULL; + + if (FollowSection == true) { + NSString *sectionKey = [NSString stringWithUTF8String : PlatformString( + Section).toMultibyte()]; + dictionary = [infoDictionary objectForKey : sectionKey]; + } else { + dictionary = infoDictionary; + } + + for (id key in dictionary) { + id option = [dictionary valueForKey : key]; + + if ([key isKindOfClass : [NSString class]] && + [option isKindOfClass : [NSString class]]) { + TString name = [key UTF8String]; + TString value = [option UTF8String]; + result->Append(Section, name, value); + } + } +} + +// Convert parts of the info.plist to the INI format the rest of the jpackage +// uses unless a jpackage config file exists. +ISectionalPropertyContainer* MacPlatform::GetConfigFile(TString FileName) { + IniFile* result = new IniFile(); + if (result == NULL) { + return NULL; + } + + if (UsePListForConfigFile() == false) { + result->LoadFromFile(FileName); + } else { + NSBundle *mainBundle = [NSBundle mainBundle]; + NSDictionary *infoDictionary = [mainBundle infoDictionary]; + std::map keys = GetKeys(); + + // JPackage options. + AppendPListDictionaryToIniFile(infoDictionary, result, + keys[CONFIG_SECTION_APPLICATION], false); + + // jvmargs + AppendPListArrayToIniFile(infoDictionary, result, + keys[CONFIG_SECTION_JAVAOPTIONS]); + + // Generate AppCDS Cache + AppendPListDictionaryToIniFile(infoDictionary, result, + keys[CONFIG_SECTION_APPCDSJAVAOPTIONS]); + AppendPListDictionaryToIniFile(infoDictionary, result, + keys[CONFIG_SECTION_APPCDSGENERATECACHEJAVAOPTIONS]); + + // args + AppendPListArrayToIniFile(infoDictionary, result, + keys[CONFIG_SECTION_ARGOPTIONS]); + } + + return result; +} + +TString GetModuleFileNameOSX() { + Dl_info module_info; + if (dladdr(reinterpret_cast (GetModuleFileNameOSX), + &module_info) == 0) { + // Failed to find the symbol we asked for. + return std::string(); + } + return TString(module_info.dli_fname); +} + +TString MacPlatform::GetModuleFileName() { + TString result; + DynamicBuffer buffer(MAX_PATH); + uint32_t size = buffer.GetSize(); + + if (_NSGetExecutablePath(buffer.GetData(), &size) == 0) { + result = FileSystemStringToString(buffer.GetData()); + } + + return result; +} + +bool MacPlatform::IsMainThread() { + bool result = (pthread_main_np() == 1); + return result; +} + +TPlatformNumber MacPlatform::GetMemorySize() { + unsigned long long memory = [[NSProcessInfo processInfo] physicalMemory]; + + // Convert from bytes to megabytes. + TPlatformNumber result = memory / 1048576; + + return result; +} + +std::map MacPlatform::GetKeys() { + std::map keys; + + if (UsePListForConfigFile() == false) { + return Platform::GetKeys(); + } else { + keys.insert(std::map::value_type(CONFIG_VERSION, + _T("app.version"))); + keys.insert(std::map::value_type(CONFIG_MAINJAR_KEY, + _T("JavaMainJarName"))); + keys.insert(std::map::value_type(CONFIG_MAINMODULE_KEY, + _T("JavaMainModuleName"))); + keys.insert(std::map::value_type( + CONFIG_MAINCLASSNAME_KEY, _T("JavaMainClassName"))); + keys.insert(std::map::value_type( + CONFIG_CLASSPATH_KEY, _T("JavaAppClasspath"))); + keys.insert(std::map::value_type(APP_NAME_KEY, + _T("CFBundleName"))); + keys.insert(std::map::value_type(JAVA_RUNTIME_KEY, + _T("JavaRuntime"))); + keys.insert(std::map::value_type(JPACKAGE_APP_DATA_DIR, + _T("CFBundleIdentifier"))); + + keys.insert(std::map::value_type(CONFIG_SPLASH_KEY, + _T("app.splash"))); + keys.insert(std::map::value_type(CONFIG_APP_MEMORY, + _T("app.memory"))); + keys.insert(std::map::value_type(CONFIG_APP_DEBUG, + _T("app.debug"))); + keys.insert(std::map::value_type( + CONFIG_APPLICATION_INSTANCE, _T("app.application.instance"))); + + keys.insert(std::map::value_type( + CONFIG_SECTION_APPLICATION, _T("Application"))); + keys.insert(std::map::value_type( + CONFIG_SECTION_JAVAOPTIONS, _T("JavaOptions"))); + keys.insert(std::map::value_type( + CONFIG_SECTION_APPCDSJAVAOPTIONS, _T("AppCDSJavaOptions"))); + keys.insert(std::map::value_type( + CONFIG_SECTION_APPCDSGENERATECACHEJAVAOPTIONS, + _T("AppCDSGenerateCacheJavaOptions"))); + keys.insert(std::map::value_type( + CONFIG_SECTION_ARGOPTIONS, _T("ArgOptions"))); + } + + return keys; +} diff --git a/src/jdk.incubator.jpackage/macosx/native/libapplauncher/PlatformDefs.h b/src/jdk.incubator.jpackage/macosx/native/libapplauncher/PlatformDefs.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/macosx/native/libapplauncher/PlatformDefs.h @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef PLATFORM_DEFS_H +#define PLATFORM_DEFS_H + +#include +#include +#include +#include +#include +#include + +using namespace std; + +#ifndef MAC +#define MAC +#endif + +#define _T(x) x + +typedef char TCHAR; +typedef std::string TString; +#define StringLength strlen + +typedef unsigned long DWORD; + +#define TRAILING_PATHSEPARATOR '/' +#define BAD_TRAILING_PATHSEPARATOR '\\' +#define PATH_SEPARATOR ':' +#define BAD_PATH_SEPARATOR ';' +#define MAX_PATH 1000 + +typedef long TPlatformNumber; +typedef pid_t TProcessID; + +#define HMODULE void* + +typedef void* Module; +typedef void* Procedure; + + +// StringToFileSystemString is a stack object. It's usage is +// simply inline to convert a +// TString to a file system string. Example: +// +// return dlopen(StringToFileSystemString(FileName), RTLD_LAZY); +// +class StringToFileSystemString { + // Prohibit Heap-Based StringToFileSystemString +private: + static void *operator new(size_t size); + static void operator delete(void *ptr); + +private: + TCHAR* FData; + bool FRelease; + +public: + StringToFileSystemString(const TString &value); + ~StringToFileSystemString(); + + operator TCHAR* (); +}; + + +// FileSystemStringToString is a stack object. It's usage is +// simply inline to convert a +// file system string to a TString. Example: +// +// DynamicBuffer buffer(MAX_PATH); +// if (readlink("/proc/self/exe", buffer.GetData(), MAX_PATH) != -1) +// result = FileSystemStringToString(buffer.GetData()); +// +class FileSystemStringToString { + // Prohibit Heap-Based FileSystemStringToString +private: + static void *operator new(size_t size); + static void operator delete(void *ptr); + +private: + TString FData; + +public: + FileSystemStringToString(const TCHAR* value); + + operator TString (); +}; + +#endif // PLATFORM_DEFS_H diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractAppImageBuilder.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractAppImageBuilder.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractAppImageBuilder.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.ArrayList; + +import jdk.incubator.jpackage.internal.resources.ResourceLocator; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + +/* + * AbstractAppImageBuilder + * This is sub-classed by each of the platform dependent AppImageBuilder + * classes, and contains resource processing code common to all platforms. + */ + +public abstract class AbstractAppImageBuilder { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MainResources"); + + private final Path root; + + public AbstractAppImageBuilder(Map unused, Path root) { + this.root = root; + } + + public InputStream getResourceAsStream(String name) { + return ResourceLocator.class.getResourceAsStream(name); + } + + public abstract void prepareApplicationFiles( + Map params) throws IOException; + public abstract void prepareJreFiles( + Map params) throws IOException; + public abstract Path getAppDir(); + public abstract Path getAppModsDir(); + + public Path getRuntimeRoot() { + return this.root; + } + + protected void copyEntry(Path appDir, File srcdir, String fname) + throws IOException { + Path dest = appDir.resolve(fname); + Files.createDirectories(dest.getParent()); + File src = new File(srcdir, fname); + if (src.isDirectory()) { + IOUtils.copyRecursive(src.toPath(), dest); + } else { + Files.copy(src.toPath(), dest); + } + } + + public void writeCfgFile(Map params, + File cfgFileName) throws IOException { + cfgFileName.getParentFile().mkdirs(); + cfgFileName.delete(); + File mainJar = JLinkBundlerHelper.getMainJar(params); + ModFile.ModType mainJarType = ModFile.ModType.Unknown; + + if (mainJar != null) { + mainJarType = new ModFile(mainJar).getModType(); + } + + String mainModule = StandardBundlerParam.MODULE.fetchFrom(params); + + try (PrintStream out = new PrintStream(cfgFileName)) { + + out.println("[Application]"); + out.println("app.name=" + APP_NAME.fetchFrom(params)); + out.println("app.version=" + VERSION.fetchFrom(params)); + out.println("app.runtime=" + getCfgRuntimeDir()); + out.println("app.identifier=" + IDENTIFIER.fetchFrom(params)); + out.println("app.classpath=" + + getCfgClassPath(CLASSPATH.fetchFrom(params))); + + // The main app is required to be a jar, modular or unnamed. + if (mainModule != null && + (mainJarType == ModFile.ModType.Unknown || + mainJarType == ModFile.ModType.ModularJar)) { + out.println("app.mainmodule=" + mainModule); + } else { + String mainClass = + StandardBundlerParam.MAIN_CLASS.fetchFrom(params); + // If the app is contained in an unnamed jar then launch it the + // legacy way and the main class string must be + // of the format com/foo/Main + if (mainJar != null) { + out.println("app.mainjar=" + getCfgAppDir() + + mainJar.toPath().getFileName().toString()); + } + if (mainClass != null) { + out.println("app.mainclass=" + + mainClass.replace("\\", "/")); + } + } + + out.println(); + out.println("[JavaOptions]"); + List jvmargs = JAVA_OPTIONS.fetchFrom(params); + for (String arg : jvmargs) { + out.println(arg); + } + Path modsDir = getAppModsDir(); + + if (modsDir != null && modsDir.toFile().exists()) { + out.println("--module-path"); + out.println(getCfgAppDir().replace("\\","/") + "mods"); + } + + out.println(); + out.println("[ArgOptions]"); + List args = ARGUMENTS.fetchFrom(params); + for (String arg : args) { + if (arg.endsWith("=") && + (arg.indexOf("=") == arg.lastIndexOf("="))) { + out.print(arg.substring(0, arg.length() - 1)); + out.println("\\="); + } else { + out.println(arg); + } + } + } + } + + File getRuntimeImageDir(File runtimeImageTop) { + return runtimeImageTop; + } + + protected String getCfgAppDir() { + return "$ROOTDIR" + File.separator + + getAppDir().getFileName() + File.separator; + } + + protected String getCfgRuntimeDir() { + return "$ROOTDIR" + File.separator + "runtime"; + } + + String getCfgClassPath(String classpath) { + String cfgAppDir = getCfgAppDir(); + + StringBuilder sb = new StringBuilder(); + for (String path : classpath.split("[:;]")) { + if (path.length() > 0) { + sb.append(cfgAppDir); + sb.append(path); + sb.append(File.pathSeparator); + } + } + if (sb.length() > 0) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractBundler.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractBundler.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + + +/** + * AbstractBundler + * + * This is the base class all bundlers extend from. + * It contains methods and parameters common to all bundlers. + * The concrete implementations are in the platform specific bundlers. + */ +abstract class AbstractBundler implements Bundler { + + static final BundlerParamInfo IMAGES_ROOT = + new StandardBundlerParam<>( + "imagesRoot", + File.class, + params -> new File( + StandardBundlerParam.TEMP_ROOT.fetchFrom(params), "images"), + (s, p) -> null); + + @Override + public String toString() { + return getName(); + } + + @Override + public void cleanup(Map params) { + try { + IOUtils.deleteRecursive( + StandardBundlerParam.TEMP_ROOT.fetchFrom(params)); + } catch (IOException e) { + Log.verbose(e.getMessage()); + } + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractImageBundler.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractImageBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractImageBundler.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.text.MessageFormat; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.io.File; +import java.io.IOException; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + +/** + * AbstractImageBundler + * + * This is the base class for each of the Application Image Bundlers. + * + * It contains methods and parameters common to all Image Bundlers. + * + * Application Image Bundlers are created in "create-app-image" mode, + * or as an intermediate step in "create-installer" mode. + * + * The concrete implementations are in the platform specific Bundlers. + */ +public abstract class AbstractImageBundler extends AbstractBundler { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MainResources"); + + public void imageBundleValidation(Map params) + throws ConfigException { + StandardBundlerParam.validateMainClassInfoFromAppResources(params); + + } + + protected File createRoot(Map params, + File outputDirectory, boolean dependentTask, String name) + throws PackagerException { + + IOUtils.writableOutputDir(outputDirectory.toPath()); + + if (!dependentTask) { + Log.verbose(MessageFormat.format( + I18N.getString("message.creating-app-bundle"), + name, outputDirectory.getAbsolutePath())); + } + + // NAME will default to CLASS, so the real problem is no MAIN_CLASS + if (name == null) { + throw new PackagerException("ERR_NoMainClass"); + } + + // Create directory structure + File rootDirectory = new File(outputDirectory, name); + + if (rootDirectory.exists()) { + throw new PackagerException("error.root-exists", + rootDirectory.getAbsolutePath()); + } + + rootDirectory.mkdirs(); + + return rootDirectory; + } + +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AddLauncherArguments.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AddLauncherArguments.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AddLauncherArguments.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.io.File; +import jdk.incubator.jpackage.internal.Arguments.CLIOptions; + +/* + * AddLauncherArguments + * + * Processes a add-launcher properties file to create the Map of + * bundle params applicable to the add-launcher: + * + * BundlerParams p = (new AddLauncherArguments(file)).getLauncherMap(); + * + * A add-launcher is another executable program generated by either the + * create-app-image mode or the create-installer mode. + * The add-launcher may be the same program with different configuration, + * or a completely different program created from the same files. + * + * There may be multiple add-launchers, each created by using the + * command line arg "--add-launcher + * + * The add-launcher properties file may have any of: + * + * appVersion + * module + * main-jar + * main-class + * icon + * arguments + * java-options + * win-console + * linux-app-category + * + */ +class AddLauncherArguments { + + private final String name; + private final String filename; + private Map allArgs; + private Map bundleParams; + + AddLauncherArguments(String name, String filename) { + this.name = name; + this.filename = filename; + } + + private void initLauncherMap() { + if (bundleParams != null) { + return; + } + + allArgs = Arguments.getPropertiesFromFile(filename); + allArgs.put(CLIOptions.NAME.getId(), name); + + bundleParams = new HashMap<>(); + String mainJar = getOptionValue(CLIOptions.MAIN_JAR); + String mainClass = getOptionValue(CLIOptions.APPCLASS); + String module = getOptionValue(CLIOptions.MODULE); + + if (module != null && mainClass != null) { + putUnlessNull(bundleParams, CLIOptions.MODULE.getId(), + module + "/" + mainClass); + } else if (module != null) { + putUnlessNull(bundleParams, CLIOptions.MODULE.getId(), + module); + } else { + putUnlessNull(bundleParams, CLIOptions.MAIN_JAR.getId(), + mainJar); + putUnlessNull(bundleParams, CLIOptions.APPCLASS.getId(), + mainClass); + } + + putUnlessNull(bundleParams, CLIOptions.NAME.getId(), + getOptionValue(CLIOptions.NAME)); + + putUnlessNull(bundleParams, CLIOptions.VERSION.getId(), + getOptionValue(CLIOptions.VERSION)); + + putUnlessNull(bundleParams, CLIOptions.RELEASE.getId(), + getOptionValue(CLIOptions.RELEASE)); + + putUnlessNull(bundleParams, CLIOptions.LINUX_CATEGORY.getId(), + getOptionValue(CLIOptions.LINUX_CATEGORY)); + + putUnlessNull(bundleParams, + CLIOptions.WIN_CONSOLE_HINT.getId(), + getOptionValue(CLIOptions.WIN_CONSOLE_HINT)); + + String value = getOptionValue(CLIOptions.ICON); + putUnlessNull(bundleParams, CLIOptions.ICON.getId(), + (value == null) ? null : new File(value)); + + // "arguments" and "java-options" even if value is null: + if (allArgs.containsKey(CLIOptions.ARGUMENTS.getId())) { + String argumentStr = getOptionValue(CLIOptions.ARGUMENTS); + bundleParams.put(CLIOptions.ARGUMENTS.getId(), + Arguments.getArgumentList(argumentStr)); + } + + if (allArgs.containsKey(CLIOptions.JAVA_OPTIONS.getId())) { + String jvmargsStr = getOptionValue(CLIOptions.JAVA_OPTIONS); + bundleParams.put(CLIOptions.JAVA_OPTIONS.getId(), + Arguments.getArgumentList(jvmargsStr)); + } + } + + private String getOptionValue(CLIOptions option) { + if (option == null || allArgs == null) { + return null; + } + + String id = option.getId(); + + if (allArgs.containsKey(id)) { + return allArgs.get(id); + } + + return null; + } + + Map getLauncherMap() { + initLauncherMap(); + return bundleParams; + } + + private void putUnlessNull(Map params, + String param, Object value) { + if (value != null) { + params.put(param, value); + } + } + + static Map merge( + Map original, + Map additional) { + Map tmp = new HashMap<>(original); + if (additional.containsKey(CLIOptions.MODULE.getId())) { + tmp.remove(CLIOptions.MAIN_JAR.getId()); + tmp.remove(CLIOptions.APPCLASS.getId()); + } else if (additional.containsKey(CLIOptions.MAIN_JAR.getId())) { + tmp.remove(CLIOptions.MODULE.getId()); + } + if (additional.containsKey(CLIOptions.ARGUMENTS.getId())) { + // if add launcher properties file contains "arguments", even with + // null value, disregard the "arguments" from command line + tmp.remove(CLIOptions.ARGUMENTS.getId()); + } + if (additional.containsKey(CLIOptions.JAVA_OPTIONS.getId())) { + // same thing for java-options + tmp.remove(CLIOptions.JAVA_OPTIONS.getId()); + } + tmp.putAll(additional); + return tmp; + } + +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AppImageFile.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AppImageFile.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AppImageFile.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + +public class AppImageFile { + + // These values will be loaded from AppImage xml file. + private final String creatorVersion; + private final String creatorPlatform; + private final String launcherName; + private final List addLauncherNames; + + private final static String FILENAME = ".jpackage.xml"; + + private final static Map PLATFORM_LABELS = Map.of( + Platform.LINUX, "linux", Platform.WINDOWS, "windows", Platform.MAC, + "macOS"); + + + private AppImageFile() { + this(null, null, null, null); + } + + private AppImageFile(String launcherName, List addLauncherNames, + String creatorVersion, String creatorPlatform) { + this.launcherName = launcherName; + this.addLauncherNames = addLauncherNames; + this.creatorVersion = creatorVersion; + this.creatorPlatform = creatorPlatform; + } + + /** + * Returns list of additional launchers configured for the application. + * Each item in the list is not null or empty string. + * Returns empty list for application without additional launchers. + */ + List getAddLauncherNames() { + return addLauncherNames; + } + + /** + * Returns main application launcher name. Never returns null or empty value. + */ + String getLauncherName() { + return launcherName; + } + + void verifyCompatible() throws ConfigException { + // Just do nothing for now. + } + + /** + * Returns path to application image info file. + * @param appImageDir - path to application image + */ + public static Path getPathInAppImage(Path appImageDir) { + return appImageDir.resolve(FILENAME); + } + + /** + * Saves file with application image info in application image. + * @param appImageDir - path to application image + * @throws IOException + */ + static void save(Path appImageDir, Map params) + throws IOException { + IOUtils.createXml(getPathInAppImage(appImageDir), xml -> { + xml.writeStartElement("jpackage-state"); + xml.writeAttribute("version", getVersion()); + xml.writeAttribute("platform", getPlatform()); + + xml.writeStartElement("main-launcher"); + xml.writeCharacters(APP_NAME.fetchFrom(params)); + xml.writeEndElement(); + + List> addLaunchers = + ADD_LAUNCHERS.fetchFrom(params); + + for (int i = 0; i < addLaunchers.size(); i++) { + Map sl = addLaunchers.get(i); + xml.writeStartElement("add-launcher"); + xml.writeCharacters(APP_NAME.fetchFrom(sl)); + xml.writeEndElement(); + } + }); + } + + /** + * Loads application image info from application image. + * @param appImageDir - path to application image + * @return valid info about application image or null + * @throws IOException + */ + static AppImageFile load(Path appImageDir) throws IOException { + try { + Path path = getPathInAppImage(appImageDir); + DocumentBuilderFactory dbf = + DocumentBuilderFactory.newDefaultInstance(); + dbf.setFeature( + "http://apache.org/xml/features/nonvalidating/load-external-dtd", + false); + DocumentBuilder b = dbf.newDocumentBuilder(); + Document doc = b.parse(new FileInputStream(path.toFile())); + + XPath xPath = XPathFactory.newInstance().newXPath(); + + String mainLauncher = xpathQueryNullable(xPath, + "/jpackage-state/main-launcher/text()", doc); + if (mainLauncher == null) { + // No main launcher, this is fatal. + return new AppImageFile(); + } + + List addLaunchers = new ArrayList(); + + String platform = xpathQueryNullable(xPath, + "/jpackage-state/@platform", doc); + + String version = xpathQueryNullable(xPath, + "/jpackage-state/@version", doc); + + NodeList launcherNameNodes = (NodeList) xPath.evaluate( + "/jpackage-state/add-launcher/text()", doc, + XPathConstants.NODESET); + + for (int i = 0; i != launcherNameNodes.getLength(); i++) { + addLaunchers.add(launcherNameNodes.item(i).getNodeValue()); + } + + AppImageFile file = new AppImageFile( + mainLauncher, addLaunchers, version, platform); + if (!file.isValid()) { + file = new AppImageFile(); + } + return file; + } catch (ParserConfigurationException | SAXException ex) { + // Let caller sort this out + throw new IOException(ex); + } catch (XPathExpressionException ex) { + // This should never happen as XPath expressions should be correct + throw new RuntimeException(ex); + } + } + + /** + * Returns list of launcher names configured for the application. + * The first item in the returned list is main launcher name. + * Following items in the list are names of additional launchers. + */ + static List getLauncherNames(Path appImageDir, + Map params) { + List launchers = new ArrayList<>(); + try { + AppImageFile appImageInfo = AppImageFile.load(appImageDir); + if (appImageInfo != null) { + launchers.add(appImageInfo.getLauncherName()); + launchers.addAll(appImageInfo.getAddLauncherNames()); + return launchers; + } + } catch (IOException ioe) { + Log.verbose(ioe); + } + + launchers.add(APP_NAME.fetchFrom(params)); + ADD_LAUNCHERS.fetchFrom(params).stream().map(APP_NAME::fetchFrom).forEach( + launchers::add); + return launchers; + } + + private static String xpathQueryNullable(XPath xPath, String xpathExpr, + Document xml) throws XPathExpressionException { + NodeList nodes = (NodeList) xPath.evaluate(xpathExpr, xml, + XPathConstants.NODESET); + if (nodes != null && nodes.getLength() > 0) { + return nodes.item(0).getNodeValue(); + } + return null; + } + + private static String getVersion() { + return System.getProperty("java.version"); + } + + private static String getPlatform() { + return PLATFORM_LABELS.get(Platform.getPlatform()); + } + + private boolean isValid() { + if (launcherName == null || launcherName.length() == 0 || + addLauncherNames.indexOf("") != -1) { + // Some launchers have empty names. This is invalid. + return false; + } + + // Add more validation. + + return true; + } + +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ApplicationLayout.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ApplicationLayout.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ApplicationLayout.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.nio.file.Path; +import java.util.Map; + + +/** + * Application directory layout. + */ +public final class ApplicationLayout implements PathGroup.Facade { + enum PathRole { + RUNTIME, APP, LAUNCHERS, DESKTOP, APP_MODS, DLLS + } + + ApplicationLayout(Map paths) { + data = new PathGroup(paths); + } + + private ApplicationLayout(PathGroup data) { + this.data = data; + } + + @Override + public PathGroup pathGroup() { + return data; + } + + @Override + public ApplicationLayout resolveAt(Path root) { + return new ApplicationLayout(pathGroup().resolveAt(root)); + } + + /** + * Path to launchers directory. + */ + public Path launchersDirectory() { + return pathGroup().getPath(PathRole.LAUNCHERS); + } + + /** + * Path to directory with dynamic libraries. + */ + public Path dllDirectory() { + return pathGroup().getPath(PathRole.DLLS); + } + + /** + * Path to application data directory. + */ + public Path appDirectory() { + return pathGroup().getPath(PathRole.APP); + } + + /** + * Path to Java runtime directory. + */ + public Path runtimeDirectory() { + return pathGroup().getPath(PathRole.RUNTIME); + } + + /** + * Path to application mods directory. + */ + public Path appModsDirectory() { + return pathGroup().getPath(PathRole.APP_MODS); + } + + /** + * Path to directory with application's desktop integration files. + */ + public Path destktopIntegrationDirectory() { + return pathGroup().getPath(PathRole.DESKTOP); + } + + static ApplicationLayout linuxAppImage() { + return new ApplicationLayout(Map.of( + PathRole.LAUNCHERS, Path.of("bin"), + PathRole.APP, Path.of("lib/app"), + PathRole.RUNTIME, Path.of("lib/runtime"), + PathRole.DESKTOP, Path.of("lib"), + PathRole.DLLS, Path.of("lib"), + PathRole.APP_MODS, Path.of("lib/app/mods") + )); + } + + static ApplicationLayout windowsAppImage() { + return new ApplicationLayout(Map.of( + PathRole.LAUNCHERS, Path.of(""), + PathRole.APP, Path.of("app"), + PathRole.RUNTIME, Path.of("runtime"), + PathRole.DESKTOP, Path.of(""), + PathRole.DLLS, Path.of(""), + PathRole.APP_MODS, Path.of("app/mods") + )); + } + + static ApplicationLayout macAppImage() { + return new ApplicationLayout(Map.of( + PathRole.LAUNCHERS, Path.of("Contents/MacOS"), + PathRole.APP, Path.of("Contents/app"), + PathRole.RUNTIME, Path.of("Contents/runtime"), + PathRole.DESKTOP, Path.of("Contents/Resources"), + PathRole.DLLS, Path.of("Contents/MacOS"), + PathRole.APP_MODS, Path.of("Contents/app/mods") + )); + } + + public static ApplicationLayout platformAppImage() { + if (Platform.isWindows()) { + return windowsAppImage(); + } + + if (Platform.isLinux()) { + return linuxAppImage(); + } + + if (Platform.isMac()) { + return macAppImage(); + } + + throw Platform.throwUnknownPlatformError(); + } + + public static ApplicationLayout javaRuntime() { + return new ApplicationLayout(Map.of(PathRole.RUNTIME, Path.of(""))); + } + + private final PathGroup data; +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ArgAction.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ArgAction.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ArgAction.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +@FunctionalInterface +interface ArgAction { + void execute(); +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Arguments.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Arguments.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Arguments.java @@ -0,0 +1,802 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Properties; +import java.util.ResourceBundle; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Arguments + * + * This class encapsulates and processes the command line arguments, + * in effect, implementing all the work of jpackage tool. + * + * The primary entry point, processArguments(): + * Processes and validates command line arguments, constructing DeployParams. + * Validates the DeployParams, and generate the BundleParams. + * Generates List of Bundlers from BundleParams valid for this platform. + * Executes each Bundler in the list. + */ +public class Arguments { + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MainResources"); + + private static final String FA_EXTENSIONS = "extension"; + private static final String FA_CONTENT_TYPE = "mime-type"; + private static final String FA_DESCRIPTION = "description"; + private static final String FA_ICON = "icon"; + + // regexp for parsing args (for example, for additional launchers) + private static Pattern pattern = Pattern.compile( + "(?:(?:([\"'])(?:\\\\\\1|.)*?(?:\\1|$))|(?:\\\\[\"'\\s]|[^\\s]))++"); + + private DeployParams deployParams = null; + + private int pos = 0; + private List argList = null; + + private List allOptions = null; + + private String input = null; + private String output = null; + + private boolean hasMainJar = false; + private boolean hasMainClass = false; + private boolean hasMainModule = false; + public boolean userProvidedBuildRoot = false; + + private String buildRoot = null; + private String mainJarPath = null; + + private static boolean runtimeInstaller = false; + + private List addLaunchers = null; + + private static Map argIds = new HashMap<>(); + private static Map argShortIds = new HashMap<>(); + + static { + // init maps for parsing arguments + (EnumSet.allOf(CLIOptions.class)).forEach(option -> { + argIds.put(option.getIdWithPrefix(), option); + if (option.getShortIdWithPrefix() != null) { + argShortIds.put(option.getShortIdWithPrefix(), option); + } + }); + } + + public Arguments(String[] args) { + argList = new ArrayList(args.length); + for (String arg : args) { + argList.add(arg); + } + Log.verbose ("\njpackage argument list: \n" + argList + "\n"); + pos = 0; + + deployParams = new DeployParams(); + + allOptions = new ArrayList<>(); + + addLaunchers = new ArrayList<>(); + + output = Paths.get("").toAbsolutePath().toString(); + deployParams.setOutput(new File(output)); + } + + // CLIOptions is public for DeployParamsTest + public enum CLIOptions { + PACKAGE_TYPE("type", "t", OptionCategories.PROPERTY, () -> { + context().deployParams.setTargetFormat(popArg()); + }), + + INPUT ("input", "i", OptionCategories.PROPERTY, () -> { + context().input = popArg(); + setOptionValue("input", context().input); + }), + + OUTPUT ("dest", "d", OptionCategories.PROPERTY, () -> { + context().output = popArg(); + context().deployParams.setOutput(new File(context().output)); + }), + + DESCRIPTION ("description", OptionCategories.PROPERTY), + + VENDOR ("vendor", OptionCategories.PROPERTY), + + APPCLASS ("main-class", OptionCategories.PROPERTY, () -> { + context().hasMainClass = true; + setOptionValue("main-class", popArg()); + }), + + NAME ("name", "n", OptionCategories.PROPERTY), + + VERBOSE ("verbose", OptionCategories.PROPERTY, () -> { + setOptionValue("verbose", true); + Log.setVerbose(); + }), + + RESOURCE_DIR("resource-dir", + OptionCategories.PROPERTY, () -> { + String resourceDir = popArg(); + setOptionValue("resource-dir", resourceDir); + }), + + ARGUMENTS ("arguments", OptionCategories.PROPERTY, () -> { + List arguments = getArgumentList(popArg()); + setOptionValue("arguments", arguments); + }), + + ICON ("icon", OptionCategories.PROPERTY), + + COPYRIGHT ("copyright", OptionCategories.PROPERTY), + + LICENSE_FILE ("license-file", OptionCategories.PROPERTY), + + VERSION ("app-version", OptionCategories.PROPERTY), + + RELEASE ("linux-app-release", OptionCategories.PROPERTY), + + JAVA_OPTIONS ("java-options", OptionCategories.PROPERTY, () -> { + List args = getArgumentList(popArg()); + args.forEach(a -> setOptionValue("java-options", a)); + }), + + FILE_ASSOCIATIONS ("file-associations", + OptionCategories.PROPERTY, () -> { + Map args = new HashMap<>(); + + // load .properties file + Map initialMap = getPropertiesFromFile(popArg()); + + String ext = initialMap.get(FA_EXTENSIONS); + if (ext != null) { + args.put(StandardBundlerParam.FA_EXTENSIONS.getID(), ext); + } + + String type = initialMap.get(FA_CONTENT_TYPE); + if (type != null) { + args.put(StandardBundlerParam.FA_CONTENT_TYPE.getID(), type); + } + + String desc = initialMap.get(FA_DESCRIPTION); + if (desc != null) { + args.put(StandardBundlerParam.FA_DESCRIPTION.getID(), desc); + } + + String icon = initialMap.get(FA_ICON); + if (icon != null) { + args.put(StandardBundlerParam.FA_ICON.getID(), icon); + } + + ArrayList> associationList = + new ArrayList>(); + + associationList.add(args); + + // check that we really add _another_ value to the list + setOptionValue("file-associations", associationList); + + }), + + ADD_LAUNCHER ("add-launcher", + OptionCategories.PROPERTY, () -> { + String spec = popArg(); + String name = null; + String filename = spec; + if (spec.contains("=")) { + String[] values = spec.split("=", 2); + name = values[0]; + filename = values[1]; + } + context().addLaunchers.add( + new AddLauncherArguments(name, filename)); + }), + + TEMP_ROOT ("temp", OptionCategories.PROPERTY, () -> { + context().buildRoot = popArg(); + context().userProvidedBuildRoot = true; + setOptionValue("temp", context().buildRoot); + }), + + INSTALL_DIR ("install-dir", OptionCategories.PROPERTY), + + PREDEFINED_APP_IMAGE ("app-image", OptionCategories.PROPERTY), + + PREDEFINED_RUNTIME_IMAGE ("runtime-image", OptionCategories.PROPERTY), + + MAIN_JAR ("main-jar", OptionCategories.PROPERTY, () -> { + context().mainJarPath = popArg(); + context().hasMainJar = true; + setOptionValue("main-jar", context().mainJarPath); + }), + + MODULE ("module", "m", OptionCategories.MODULAR, () -> { + context().hasMainModule = true; + setOptionValue("module", popArg()); + }), + + ADD_MODULES ("add-modules", OptionCategories.MODULAR), + + MODULE_PATH ("module-path", "p", OptionCategories.MODULAR), + + BIND_SERVICES ("bind-services", OptionCategories.PROPERTY, () -> { + setOptionValue("bind-services", true); + }), + + MAC_SIGN ("mac-sign", "s", OptionCategories.PLATFORM_MAC, () -> { + setOptionValue("mac-sign", true); + }), + + MAC_BUNDLE_NAME ("mac-package-name", OptionCategories.PLATFORM_MAC), + + MAC_BUNDLE_IDENTIFIER("mac-package-identifier", + OptionCategories.PLATFORM_MAC), + + MAC_BUNDLE_SIGNING_PREFIX ("mac-package-signing-prefix", + OptionCategories.PLATFORM_MAC), + + MAC_SIGNING_KEY_NAME ("mac-signing-key-user-name", + OptionCategories.PLATFORM_MAC), + + MAC_SIGNING_KEYCHAIN ("mac-signing-keychain", + OptionCategories.PLATFORM_MAC), + + MAC_APP_STORE_ENTITLEMENTS ("mac-app-store-entitlements", + OptionCategories.PLATFORM_MAC), + + WIN_MENU_HINT ("win-menu", OptionCategories.PLATFORM_WIN, () -> { + setOptionValue("win-menu", true); + }), + + WIN_MENU_GROUP ("win-menu-group", OptionCategories.PLATFORM_WIN), + + WIN_SHORTCUT_HINT ("win-shortcut", + OptionCategories.PLATFORM_WIN, () -> { + setOptionValue("win-shortcut", true); + }), + + WIN_PER_USER_INSTALLATION ("win-per-user-install", + OptionCategories.PLATFORM_WIN, () -> { + setOptionValue("win-per-user-install", false); + }), + + WIN_DIR_CHOOSER ("win-dir-chooser", + OptionCategories.PLATFORM_WIN, () -> { + setOptionValue("win-dir-chooser", true); + }), + + WIN_UPGRADE_UUID ("win-upgrade-uuid", + OptionCategories.PLATFORM_WIN), + + WIN_CONSOLE_HINT ("win-console", OptionCategories.PLATFORM_WIN, () -> { + setOptionValue("win-console", true); + }), + + LINUX_BUNDLE_NAME ("linux-package-name", + OptionCategories.PLATFORM_LINUX), + + LINUX_DEB_MAINTAINER ("linux-deb-maintainer", + OptionCategories.PLATFORM_LINUX), + + LINUX_CATEGORY ("linux-app-category", + OptionCategories.PLATFORM_LINUX), + + LINUX_RPM_LICENSE_TYPE ("linux-rpm-license-type", + OptionCategories.PLATFORM_LINUX), + + LINUX_PACKAGE_DEPENDENCIES ("linux-package-deps", + OptionCategories.PLATFORM_LINUX), + + LINUX_SHORTCUT_HINT ("linux-shortcut", + OptionCategories.PLATFORM_LINUX, () -> { + setOptionValue("linux-shortcut", true); + }), + + LINUX_MENU_GROUP ("linux-menu-group", OptionCategories.PLATFORM_LINUX); + + private final String id; + private final String shortId; + private final OptionCategories category; + private final ArgAction action; + private static Arguments argContext; + + private CLIOptions(String id, OptionCategories category) { + this(id, null, category, null); + } + + private CLIOptions(String id, String shortId, + OptionCategories category) { + this(id, shortId, category, null); + } + + private CLIOptions(String id, + OptionCategories category, ArgAction action) { + this(id, null, category, action); + } + + private CLIOptions(String id, String shortId, + OptionCategories category, ArgAction action) { + this.id = id; + this.shortId = shortId; + this.action = action; + this.category = category; + } + + static void setContext(Arguments context) { + argContext = context; + } + + public static Arguments context() { + if (argContext != null) { + return argContext; + } else { + throw new RuntimeException("Argument context is not set."); + } + } + + public String getId() { + return this.id; + } + + String getIdWithPrefix() { + return "--" + this.id; + } + + String getShortIdWithPrefix() { + return this.shortId == null ? null : "-" + this.shortId; + } + + void execute() { + if (action != null) { + action.execute(); + } else { + defaultAction(); + } + } + + private void defaultAction() { + context().deployParams.addBundleArgument(id, popArg()); + } + + private static void setOptionValue(String option, Object value) { + context().deployParams.addBundleArgument(option, value); + } + + private static String popArg() { + nextArg(); + return (context().pos >= context().argList.size()) ? + "" : context().argList.get(context().pos); + } + + private static String getArg() { + return (context().pos >= context().argList.size()) ? + "" : context().argList.get(context().pos); + } + + private static void nextArg() { + context().pos++; + } + + private static boolean hasNextArg() { + return context().pos < context().argList.size(); + } + } + + enum OptionCategories { + MODULAR, + PROPERTY, + PLATFORM_MAC, + PLATFORM_WIN, + PLATFORM_LINUX; + } + + public boolean processArguments() { + try { + + // init context of arguments + CLIOptions.setContext(this); + + // parse cmd line + String arg; + CLIOptions option; + for (; CLIOptions.hasNextArg(); CLIOptions.nextArg()) { + arg = CLIOptions.getArg(); + if ((option = toCLIOption(arg)) != null) { + // found a CLI option + allOptions.add(option); + option.execute(); + } else { + throw new PackagerException("ERR_InvalidOption", arg); + } + } + + if (hasMainJar && !hasMainClass) { + // try to get main-class from manifest + String mainClass = getMainClassFromManifest(); + if (mainClass != null) { + CLIOptions.setOptionValue( + CLIOptions.APPCLASS.getId(), mainClass); + } + } + + // display error for arguments that are not supported + // for current configuration. + + validateArguments(); + + addResources(deployParams, input, mainJarPath); + + List> launchersAsMap = + new ArrayList<>(); + + for (AddLauncherArguments sl : addLaunchers) { + launchersAsMap.add(sl.getLauncherMap()); + } + + deployParams.addBundleArgument( + StandardBundlerParam.ADD_LAUNCHERS.getID(), + launchersAsMap); + + // at this point deployParams should be already configured + + deployParams.validate(); + + BundleParams bp = deployParams.getBundleParams(); + + // validate name(s) + ArrayList usedNames = new ArrayList(); + usedNames.add(bp.getName()); // add main app name + + for (AddLauncherArguments sl : addLaunchers) { + Map slMap = sl.getLauncherMap(); + String slName = + (String) slMap.get(Arguments.CLIOptions.NAME.getId()); + if (slName == null) { + throw new PackagerException("ERR_NoAddLauncherName"); + } + // same rules apply to additional launcher names as app name + DeployParams.validateName(slName, false); + for (String usedName : usedNames) { + if (slName.equals(usedName)) { + throw new PackagerException("ERR_NoUniqueName"); + } + } + usedNames.add(slName); + } + if (runtimeInstaller && bp.getName() == null) { + throw new PackagerException("ERR_NoJreInstallerName"); + } + + generateBundle(bp.getBundleParamsAsMap()); + return true; + } catch (Exception e) { + if (Log.isVerbose()) { + Log.verbose(e); + } else { + String msg1 = e.getMessage(); + Log.error(msg1); + if (e.getCause() != null && e.getCause() != e) { + String msg2 = e.getCause().getMessage(); + if (msg2 != null && !msg1.contains(msg2)) { + Log.error(msg2); + } + } + } + return false; + } + } + + private void validateArguments() throws PackagerException { + String type = deployParams.getTargetFormat(); + String ptype = (type != null) ? type : "default"; + boolean imageOnly = deployParams.isTargetAppImage(); + boolean hasAppImage = allOptions.contains( + CLIOptions.PREDEFINED_APP_IMAGE); + boolean hasRuntime = allOptions.contains( + CLIOptions.PREDEFINED_RUNTIME_IMAGE); + boolean installerOnly = !imageOnly && hasAppImage; + runtimeInstaller = !imageOnly && hasRuntime && !hasAppImage && + !hasMainModule && !hasMainJar; + + for (CLIOptions option : allOptions) { + if (!ValidOptions.checkIfSupported(option)) { + // includes option valid only on different platform + throw new PackagerException("ERR_UnsupportedOption", + option.getIdWithPrefix()); + } + if (imageOnly) { + if (!ValidOptions.checkIfImageSupported(option)) { + throw new PackagerException("ERR_InvalidTypeOption", + option.getIdWithPrefix(), type); + } + } else if (installerOnly || runtimeInstaller) { + if (!ValidOptions.checkIfInstallerSupported(option)) { + if (runtimeInstaller) { + throw new PackagerException("ERR_NoInstallerEntryPoint", + option.getIdWithPrefix()); + } else { + throw new PackagerException("ERR_InvalidTypeOption", + option.getIdWithPrefix(), ptype); + } + } + } + } + if (installerOnly && hasRuntime) { + // note --runtime-image is only for image or runtime installer. + throw new PackagerException("ERR_InvalidTypeOption", + CLIOptions.PREDEFINED_RUNTIME_IMAGE.getIdWithPrefix(), + ptype); + } + if (hasMainJar && hasMainModule) { + throw new PackagerException("ERR_BothMainJarAndModule"); + } + if (imageOnly && !hasMainJar && !hasMainModule) { + throw new PackagerException("ERR_NoEntryPoint"); + } + } + + private jdk.incubator.jpackage.internal.Bundler getPlatformBundler() { + boolean appImage = deployParams.isTargetAppImage(); + String type = deployParams.getTargetFormat(); + String bundleType = (appImage ? "IMAGE" : "INSTALLER"); + + for (jdk.incubator.jpackage.internal.Bundler bundler : + Bundlers.createBundlersInstance().getBundlers(bundleType)) { + if (type == null) { + if (bundler.isDefault() + && bundler.supported(runtimeInstaller)) { + return bundler; + } + } else { + if ((appImage || type.equalsIgnoreCase(bundler.getID())) + && bundler.supported(runtimeInstaller)) { + return bundler; + } + } + } + return null; + } + + private void generateBundle(Map params) + throws PackagerException { + + boolean bundleCreated = false; + + // the temp dir needs to be fetched from the params early, + // to prevent each copy of the params (such as may be used for + // additional launchers) from generating a separate temp dir when + // the default is used (the default is a new temp directory) + // The bundler.cleanup() below would not otherwise be able to + // clean these extra (and unneeded) temp directories. + StandardBundlerParam.TEMP_ROOT.fetchFrom(params); + + // determine what bundler to run + jdk.incubator.jpackage.internal.Bundler bundler = getPlatformBundler(); + + if (bundler == null) { + throw new PackagerException("ERR_InvalidInstallerType", + deployParams.getTargetFormat()); + } + + Map localParams = new HashMap<>(params); + try { + bundler.validate(localParams); + File result = bundler.execute(localParams, deployParams.outdir); + if (result == null) { + throw new PackagerException("MSG_BundlerFailed", + bundler.getID(), bundler.getName()); + } + Log.verbose(MessageFormat.format( + I18N.getString("message.bundle-created"), + bundler.getName())); + } catch (ConfigException e) { + Log.verbose(e); + if (e.getAdvice() != null) { + throw new PackagerException(e, "MSG_BundlerConfigException", + bundler.getName(), e.getMessage(), e.getAdvice()); + } else { + throw new PackagerException(e, + "MSG_BundlerConfigExceptionNoAdvice", + bundler.getName(), e.getMessage()); + } + } catch (RuntimeException re) { + Log.verbose(re); + throw new PackagerException(re, "MSG_BundlerRuntimeException", + bundler.getName(), re.toString()); + } finally { + if (userProvidedBuildRoot) { + Log.verbose(MessageFormat.format( + I18N.getString("message.debug-working-directory"), + (new File(buildRoot)).getAbsolutePath())); + } else { + // always clean up the temporary directory created + // when --temp option not used. + bundler.cleanup(localParams); + } + } + } + + private void addResources(DeployParams deployParams, + String inputdir, String mainJar) throws PackagerException { + + if (inputdir == null || inputdir.isEmpty()) { + return; + } + + File baseDir = new File(inputdir); + + if (!baseDir.isDirectory()) { + throw new PackagerException("ERR_InputNotDirectory", inputdir); + } + if (!baseDir.canRead()) { + throw new PackagerException("ERR_CannotReadInputDir", inputdir); + } + + List fileNames; + fileNames = new ArrayList<>(); + try (Stream files = Files.list(baseDir.toPath())) { + files.forEach(file -> fileNames.add( + file.getFileName().toString())); + } catch (IOException e) { + Log.error("Unable to add resources: " + e.getMessage()); + } + fileNames.forEach(file -> deployParams.addResource(baseDir, file)); + + deployParams.setClasspath(mainJar); + } + + static CLIOptions toCLIOption(String arg) { + CLIOptions option; + if ((option = argIds.get(arg)) == null) { + option = argShortIds.get(arg); + } + return option; + } + + static Map getPropertiesFromFile(String filename) { + Map map = new HashMap<>(); + // load properties file + File file = new File(filename); + Properties properties = new Properties(); + try (FileInputStream in = new FileInputStream(file)) { + properties.load(in); + } catch (IOException e) { + Log.error("Exception: " + e.getMessage()); + } + + for (final String name: properties.stringPropertyNames()) { + map.put(name, properties.getProperty(name)); + } + + return map; + } + + static List getArgumentList(String inputString) { + List list = new ArrayList<>(); + if (inputString == null || inputString.isEmpty()) { + return list; + } + + // The "pattern" regexp attempts to abide to the rule that + // strings are delimited by whitespace unless surrounded by + // quotes, then it is anything (including spaces) in the quotes. + Matcher m = pattern.matcher(inputString); + while (m.find()) { + String s = inputString.substring(m.start(), m.end()).trim(); + // Ensure we do not have an empty string. trim() will take care of + // whitespace only strings. The regex preserves quotes and escaped + // chars so we need to clean them before adding to the List + if (!s.isEmpty()) { + list.add(unquoteIfNeeded(s)); + } + } + return list; + } + + private static String unquoteIfNeeded(String in) { + if (in == null) { + return null; + } + + if (in.isEmpty()) { + return ""; + } + + // Use code points to preserve non-ASCII chars + StringBuilder sb = new StringBuilder(); + int codeLen = in.codePointCount(0, in.length()); + int quoteChar = -1; + for (int i = 0; i < codeLen; i++) { + int code = in.codePointAt(i); + if (code == '"' || code == '\'') { + // If quote is escaped make sure to copy it + if (i > 0 && in.codePointAt(i - 1) == '\\') { + sb.deleteCharAt(sb.length() - 1); + sb.appendCodePoint(code); + continue; + } + if (quoteChar != -1) { + if (code == quoteChar) { + // close quote, skip char + quoteChar = -1; + } else { + sb.appendCodePoint(code); + } + } else { + // opening quote, skip char + quoteChar = code; + } + } else { + sb.appendCodePoint(code); + } + } + return sb.toString(); + } + + private String getMainClassFromManifest() { + if (mainJarPath == null || + input == null ) { + return null; + } + + JarFile jf; + try { + File file = new File(input, mainJarPath); + if (!file.exists()) { + return null; + } + jf = new JarFile(file); + Manifest m = jf.getManifest(); + Attributes attrs = (m != null) ? m.getMainAttributes() : null; + if (attrs != null) { + return attrs.getValue(Attributes.Name.MAIN_CLASS); + } + } catch (IOException ignore) {} + return null; + } + +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/BasicBundlers.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/BasicBundlers.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/BasicBundlers.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.ServiceLoader; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * BasicBundlers + * + * A basic bundlers collection that loads the default bundlers. + * Loads the common bundlers. + *

    + *
  • Windows file image
  • + *
  • Mac .app
  • + *
  • Linux file image
  • + *
  • Windows MSI
  • + *
  • Windows EXE
  • + *
  • Mac DMG
  • + *
  • Mac PKG
  • + *
  • Linux DEB
  • + *
  • Linux RPM
  • + * + *
+ */ +public class BasicBundlers implements Bundlers { + + boolean defaultsLoaded = false; + + private final Collection bundlers = new CopyOnWriteArrayList<>(); + + @Override + public Collection getBundlers() { + return Collections.unmodifiableCollection(bundlers); + } + + @Override + public Collection getBundlers(String type) { + if (type == null) return Collections.emptySet(); + switch (type) { + case "NONE": + return Collections.emptySet(); + case "ALL": + return getBundlers(); + default: + return Arrays.asList(getBundlers().stream() + .filter(b -> type.equalsIgnoreCase(b.getBundleType())) + .toArray(Bundler[]::new)); + } + } + + // Loads bundlers from the META-INF/services direct + @Override + public void loadBundlersFromServices(ClassLoader cl) { + ServiceLoader loader = ServiceLoader.load(Bundler.class, cl); + for (Bundler aLoader : loader) { + bundlers.add(aLoader); + } + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/BundleParams.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/BundleParams.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/BundleParams.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + +public class BundleParams { + + final protected Map params; + + // RelativeFileSet + public static final String PARAM_APP_RESOURCES = "appResources"; + + // String - Icon file name + public static final String PARAM_ICON = "icon"; + + // String - Name of bundle file and native launcher + public static final String PARAM_NAME = "name"; + + // String - application vendor, used by most of the bundlers + public static final String PARAM_VENDOR = "vendor"; + + // String - email name and email, only used for debian */ + public static final String PARAM_EMAIL = "email"; + + // String - vendor , only used for debian */ + public static final String PARAM_MAINTAINER = "maintainer"; + + /* String - Copyright. Used on Mac */ + public static final String PARAM_COPYRIGHT = "copyright"; + + // String - GUID on windows for MSI, CFBundleIdentifier on Mac + // If not compatible with requirements then bundler either do not bundle + // or autogenerate + public static final String PARAM_IDENTIFIER = "identifier"; + + /* boolean - shortcut preferences */ + public static final String PARAM_SHORTCUT = "shortcutHint"; + // boolean - menu shortcut preference + public static final String PARAM_MENU = "menuHint"; + + // String - Application version. Format may differ for different bundlers + public static final String PARAM_VERSION = "appVersion"; + + // String - Application release. Used on Linux. + public static final String PARAM_RELEASE = "appRelease"; + + // String - Optional application description. Used by MSI and on Linux + public static final String PARAM_DESCRIPTION = "description"; + + // String - License type. Needed on Linux (rpm) + public static final String PARAM_LICENSE_TYPE = "licenseType"; + + // String - File with license. Format is OS/bundler specific + public static final String PARAM_LICENSE_FILE = "licenseFile"; + + // String Main application class. + // Not used directly but used to derive default values + public static final String PARAM_APPLICATION_CLASS = "applicationClass"; + + // boolean - Adds a dialog to let the user choose a directory + // where the product will be installed. + public static final String PARAM_INSTALLDIR_CHOOSER = "installdirChooser"; + + /** + * create a new bundle with all default values + */ + public BundleParams() { + params = new HashMap<>(); + } + + /** + * Create a bundle params with a copy of the params + * @param params map of initial parameters to be copied in. + */ + public BundleParams(Map params) { + this.params = new HashMap<>(params); + } + + public void addAllBundleParams(Map params) { + this.params.putAll(params); + } + + // NOTE: we do not care about application parameters here + // as they will be embeded into jar file manifest and + // java launcher will take care of them! + + public Map getBundleParamsAsMap() { + return new HashMap<>(params); + } + + public String getName() { + return APP_NAME.fetchFrom(params); + } + + public void setAppResourcesList( + List rfs) { + putUnlessNull(APP_RESOURCES_LIST.getID(), rfs); + } + + private void putUnlessNull(String param, Object value) { + if (value != null) { + params.put(param, value); + } + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Bundler.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Bundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Bundler.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.util.Collection; +import java.util.Map; + +/** + * Bundler + * + * The basic interface implemented by all Bundlers. + */ +public interface Bundler { + /** + * @return User Friendly name of this bundler. + */ + String getName(); + + /** + * @return Command line identifier of the bundler. Should be unique. + */ + String getID(); + + /** + * @return The bundle type of the bundle that is created by this bundler. + */ + String getBundleType(); + + /** + * Determines if this bundler will execute with the given parameters. + * + * @param params The parameters to be validate. Validation may modify + * the map, so if you are going to be using the same map + * across multiple bundlers you should pass in a deep copy. + * @return true if valid + * @throws ConfigException If the configuration params are incorrect. The + * exception may contain advice on how to modify the params map + * to make it valid. + */ + public boolean validate(Map params) + throws ConfigException; + + /** + * Creates a bundle from existing content. + * + * If a call to {@link #validate(java.util.Map)} date} returns true with + * the parameters map, then you can expect a valid output. + * However if an exception was thrown out of validate or it returned + * false then you should not expect sensible results from this call. + * It may or may not return a value, and it may or may not throw an + * exception. But any output should not be considered valid or sane. + * + * @param params The Bundle parameters, + * Keyed by the id from the ParamInfo. Execution may + * modify the map, so if you are going to be using the + * same map across multiple bundlers you should pass + * in a deep copy. + * @param outputParentDir + * The parent dir that the returned bundle will be placed in. + * @return The resulting bundled file + * + * For a bundler that produces a single artifact file this will be the + * location of that artifact (.exe file, .deb file, etc) + * + * For a bundler that produces a specific directory format output this will + * be the location of that specific directory (.app file, etc). + * + * For a bundler that produce multiple files, this will be a parent + * directory of those files (linux and windows images), whose name is not + * relevant to the result. + * + * @throws java.lang.IllegalArgumentException for any of the following + * reasons: + *
    + *
  • A required parameter is not found in the params list, for + * example missing the main class.
  • + *
  • A parameter has the wrong type of an object, for example a + * String where a File is required
  • + *
  • Bundler specific incompatibilities with the parameters, for + * example a bad version number format or an application id with + * forward slashes.
  • + *
+ */ + public File execute(Map params, + File outputParentDir) throws PackagerException; + + /** + * Removes temporary files that are used for bundling. + */ + public void cleanup(Map params); + + /** + * Returns "true" if this bundler is supported on current platform. + */ + public boolean supported(boolean runtimeInstaller); + + /** + * Returns "true" if this bundler is he default for the current platform. + */ + public boolean isDefault(); +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/BundlerParamInfo.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/BundlerParamInfo.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/BundlerParamInfo.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * BundlerParamInfo + * + * A BundlerParamInfo encapsulates an individual bundler parameter of type . + */ +class BundlerParamInfo { + + /** + * The command line and hashmap name of the parameter + */ + String id; + + /** + * Type of the parameter + */ + Class valueType; + + /** + * Indicates if value was set using default value function + */ + boolean isDefaultValue; + + /** + * If the value is not set, and no fallback value is found, + * the parameter uses the value returned by the producer. + */ + Function, T> defaultValueFunction; + + /** + * An optional string converter for command line arguments. + */ + BiFunction, T> stringConverter; + + String getID() { + return id; + } + + Class getValueType() { + return valueType; + } + + boolean getIsDefaultValue() { + return isDefaultValue; + } + + Function, T> getDefaultValueFunction() { + return defaultValueFunction; + } + + BiFunction,T> + getStringConverter() { + return stringConverter; + } + + @SuppressWarnings("unchecked") + final T fetchFrom(Map params) { + return fetchFrom(params, true); + } + + @SuppressWarnings("unchecked") + final T fetchFrom(Map params, + boolean invokeDefault) { + Object o = params.get(getID()); + if (o instanceof String && getStringConverter() != null) { + return getStringConverter().apply((String)o, params); + } + + Class klass = getValueType(); + if (klass.isInstance(o)) { + return (T) o; + } + if (o != null) { + throw new IllegalArgumentException("Param " + getID() + + " should be of type " + getValueType() + + " but is a " + o.getClass()); + } + if (params.containsKey(getID())) { + // explicit nulls are allowed + return null; + } + + if (invokeDefault && (getDefaultValueFunction() != null)) { + T result = getDefaultValueFunction().apply(params); + if (result != null) { + params.put(getID(), result); + isDefaultValue = true; + } + return result; + } + + // ultimate fallback + return null; + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Bundlers.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Bundlers.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Bundlers.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.util.Collection; +import java.util.Iterator; +import java.util.ServiceLoader; + +/** + * Bundlers + * + * The interface implemented by BasicBundlers + */ +public interface Bundlers { + + /** + * This convenience method will call + * {@link #createBundlersInstance(ClassLoader)} + * with the classloader that this Bundlers is loaded from. + * + * @return an instance of Bundlers loaded and configured from + * the current ClassLoader. + */ + public static Bundlers createBundlersInstance() { + return createBundlersInstance(Bundlers.class.getClassLoader()); + } + + /** + * This convenience method will automatically load a Bundlers instance + * from either META-INF/services or the default + * {@link BasicBundlers} if none are found in + * the services meta-inf. + * + * After instantiating the bundlers instance it will load the default + * bundlers via {@link #loadDefaultBundlers()} as well as requesting + * the services loader to load any other bundelrs via + * {@link #loadBundlersFromServices(ClassLoader)}. + + * + * @param servicesClassLoader the classloader to search for + * META-INF/service registered bundlers + * @return an instance of Bundlers loaded and configured from + * the specified ClassLoader + */ + public static Bundlers createBundlersInstance( + ClassLoader servicesClassLoader) { + ServiceLoader bundlersLoader = + ServiceLoader.load(Bundlers.class, servicesClassLoader); + Bundlers bundlers = null; + Iterator iter = bundlersLoader.iterator(); + if (iter.hasNext()) { + bundlers = iter.next(); + } + if (bundlers == null) { + bundlers = new BasicBundlers(); + } + + bundlers.loadBundlersFromServices(servicesClassLoader); + return bundlers; + } + + /** + * Returns all of the preconfigured, requested, and manually + * configured bundlers loaded with this instance. + * + * @return a read-only collection of the requested bundlers + */ + Collection getBundlers(); + + /** + * Returns all of the preconfigured, requested, and manually + * configured bundlers loaded with this instance that are of + * a specific BundleType, such as disk images, installers, or + * remote installers. + * + * @return a read-only collection of the requested bundlers + */ + Collection getBundlers(String type); + + /** + * Loads bundlers from the META-INF/services directly. + * + * This method is called from the + * {@link #createBundlersInstance(ClassLoader)} + * and {@link #createBundlersInstance()} methods. + */ + void loadBundlersFromServices(ClassLoader cl); + +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/CLIHelp.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/CLIHelp.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/CLIHelp.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.util.ResourceBundle; +import java.io.File; +import java.text.MessageFormat; + + +/** + * CLIHelp + * + * Generate and show the command line interface help message(s). + */ +public class CLIHelp { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.HelpResources"); + + // generates --help for jpackage's CLI + public static void showHelp(boolean noArgs) { + + if (noArgs) { + Log.info(I18N.getString("MSG_Help_no_args")); + } else { + Platform platform = (Log.isVerbose()) ? + Platform.UNKNOWN : Platform.getPlatform(); + String types; + String pLaunchOptions; + String pInstallOptions; + String pInstallDir; + switch (platform) { + case MAC: + types = "{\"app-image\", \"dmg\", \"pkg\"}"; + pLaunchOptions = I18N.getString("MSG_Help_mac_launcher"); + pInstallOptions = ""; + pInstallDir + = I18N.getString("MSG_Help_mac_linux_install_dir"); + break; + case LINUX: + types = "{\"app-image\", \"rpm\", \"deb\"}"; + pLaunchOptions = ""; + pInstallOptions = I18N.getString("MSG_Help_linux_install"); + pInstallDir + = I18N.getString("MSG_Help_mac_linux_install_dir"); + break; + case WINDOWS: + types = "{\"app-image\", \"exe\", \"msi\"}"; + pLaunchOptions = I18N.getString("MSG_Help_win_launcher"); + pInstallOptions = I18N.getString("MSG_Help_win_install"); + pInstallDir + = I18N.getString("MSG_Help_win_install_dir"); + break; + default: + types = "{\"app-image\", \"exe\", \"msi\", \"rpm\", \"deb\", \"pkg\", \"dmg\"}"; + pLaunchOptions = I18N.getString("MSG_Help_win_launcher") + + I18N.getString("MSG_Help_mac_launcher"); + pInstallOptions = I18N.getString("MSG_Help_win_install") + + I18N.getString("MSG_Help_linux_install"); + pInstallDir + = I18N.getString("MSG_Help_default_install_dir"); + break; + } + Log.info(MessageFormat.format(I18N.getString("MSG_Help"), + File.pathSeparator, types, pLaunchOptions, + pInstallOptions, pInstallDir)); + } + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ConfigException.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ConfigException.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ConfigException.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +public class ConfigException extends Exception { + private static final long serialVersionUID = 1L; + final String advice; + + public ConfigException(String msg, String advice) { + super(msg); + this.advice = advice; + } + + public ConfigException(String msg, String advice, Exception cause) { + super(msg, cause); + this.advice = advice; + } + + public ConfigException(Exception cause) { + super(cause); + this.advice = null; + } + + public String getAdvice() { + return advice; + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/DeployParams.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/DeployParams.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/DeployParams.java @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2011, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.InvalidPathException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * DeployParams + * + * This class is generated and used in Arguments.processArguments() as + * intermediate step in generating the BundleParams and ultimately the Bundles + */ +public class DeployParams { + + final List resources = new ArrayList<>(); + + String targetFormat = null; // means default type for this platform + + File outdir = null; + + // raw arguments to the bundler + Map bundlerArguments = new LinkedHashMap<>(); + + public void setOutput(File output) { + outdir = output; + } + + static class Template { + File in; + File out; + + Template(File in, File out) { + this.in = in; + this.out = out; + } + } + + // we need to expand as in some cases + // (most notably jpackage) + // we may get "." as filename and assumption is we include + // everything in the given folder + // (IOUtils.copyfiles() have recursive behavior) + List expandFileset(File root) { + List files = new LinkedList<>(); + if (!Files.isSymbolicLink(root.toPath())) { + if (root.isDirectory()) { + File[] children = root.listFiles(); + if (children != null) { + for (File f : children) { + files.addAll(expandFileset(f)); + } + } + } else { + files.add(root); + } + } + return files; + } + + public void addResource(File baseDir, String path) { + addResource(baseDir, new File(baseDir, path)); + } + + public void addResource(File baseDir, File file) { + // normalize initial file + // to strip things like "." in the path + // or it can confuse symlink detection logic + file = file.getAbsoluteFile(); + + if (baseDir == null) { + baseDir = file.getParentFile(); + } + resources.add(new RelativeFileSet( + baseDir, new LinkedHashSet<>(expandFileset(file)))); + } + + void setClasspath(String mainJarPath) { + String classpath; + // we want main jar first on the classpath + if (mainJarPath != null) { + classpath = mainJarPath + File.pathSeparator; + } else { + classpath = ""; + } + for (RelativeFileSet resource : resources) { + for (String file : resource.getIncludedFiles()) { + if (file.endsWith(".jar")) { + if (!file.equals(mainJarPath)) { + classpath += file + File.pathSeparator; + } + } + } + } + addBundleArgument( + StandardBundlerParam.CLASSPATH.getID(), classpath); + } + + static void validateName(String s, boolean forApp) + throws PackagerException { + + String exceptionKey = forApp ? + "ERR_InvalidAppName" : "ERR_InvalidSLName"; + + if (s == null) { + if (forApp) { + return; + } else { + throw new PackagerException(exceptionKey, s); + } + } + if (s.length() == 0 || s.charAt(s.length() - 1) == '\\') { + throw new PackagerException(exceptionKey, s); + } + try { + // name must be valid path element for this file system + Path p = (new File(s)).toPath(); + // and it must be a single name element in a path + if (p.getNameCount() != 1) { + throw new PackagerException(exceptionKey, s); + } + } catch (InvalidPathException ipe) { + throw new PackagerException(ipe, exceptionKey, s); + } + + for (int i = 0; i < s.length(); i++) { + char a = s.charAt(i); + // We check for ASCII codes first which we accept. If check fails, + // check if it is acceptable extended ASCII or unicode character. + if (a < ' ' || a > '~') { + // Accept anything else including special chars like copyright + // symbols. Note: space will be included by ASCII check above, + // but other whitespace like tabs or new line will be rejected. + if (Character.isISOControl(a) || + Character.isWhitespace(a)) { + throw new PackagerException(exceptionKey, s); + } + } else if (a == '"' || a == '%') { + throw new PackagerException(exceptionKey, s); + } + } + } + + public void validate() throws PackagerException { + boolean hasModule = (bundlerArguments.get( + Arguments.CLIOptions.MODULE.getId()) != null); + boolean hasAppImage = (bundlerArguments.get( + Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId()) != null); + boolean hasClass = (bundlerArguments.get( + Arguments.CLIOptions.APPCLASS.getId()) != null); + boolean hasMain = (bundlerArguments.get( + Arguments.CLIOptions.MAIN_JAR.getId()) != null); + boolean hasRuntimeImage = (bundlerArguments.get( + Arguments.CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId()) != null); + boolean hasInput = (bundlerArguments.get( + Arguments.CLIOptions.INPUT.getId()) != null); + boolean hasModulePath = (bundlerArguments.get( + Arguments.CLIOptions.MODULE_PATH.getId()) != null); + boolean runtimeInstaller = !isTargetAppImage() && + !hasAppImage && !hasModule && !hasMain && hasRuntimeImage; + + if (isTargetAppImage()) { + // Module application requires --runtime-image or --module-path + if (hasModule) { + if (!hasModulePath && !hasRuntimeImage) { + throw new PackagerException("ERR_MissingArgument", + "--runtime-image or --module-path"); + } + } else { + if (!hasInput) { + throw new PackagerException( + "ERR_MissingArgument", "--input"); + } + } + } else { + if (!runtimeInstaller) { + if (hasModule) { + if (!hasModulePath && !hasRuntimeImage && !hasAppImage) { + throw new PackagerException("ERR_MissingArgument", + "--runtime-image, --module-path or --app-image"); + } + } else { + if (!hasInput && !hasAppImage) { + throw new PackagerException("ERR_MissingArgument", + "--input or --app-image"); + } + } + } + } + + // if bundling non-modular image, or installer without app-image + // then we need some resources and a main class + if (!hasModule && !hasAppImage && !runtimeInstaller) { + if (resources.isEmpty()) { + throw new PackagerException("ERR_MissingAppResources"); + } + if (!hasMain) { + throw new PackagerException("ERR_MissingArgument", + "--main-jar"); + } + } + + String name = (String)bundlerArguments.get( + Arguments.CLIOptions.NAME.getId()); + validateName(name, true); + + // Validate app image if set + String appImage = (String)bundlerArguments.get( + Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId()); + if (appImage != null) { + File appImageDir = new File(appImage); + if (!appImageDir.exists() || appImageDir.list().length == 0) { + throw new PackagerException("ERR_AppImageNotExist", appImage); + } + } + + // Validate temp dir + String root = (String)bundlerArguments.get( + Arguments.CLIOptions.TEMP_ROOT.getId()); + if (root != null) { + String [] contents = (new File(root)).list(); + + if (contents != null && contents.length > 0) { + throw new PackagerException("ERR_BuildRootInvalid", root); + } + } + + // Validate license file if set + String license = (String)bundlerArguments.get( + Arguments.CLIOptions.LICENSE_FILE.getId()); + if (license != null) { + File licenseFile = new File(license); + if (!licenseFile.exists()) { + throw new PackagerException("ERR_LicenseFileNotExit"); + } + } + } + + void setTargetFormat(String t) { + targetFormat = t; + } + + String getTargetFormat() { + return targetFormat; + } + + boolean isTargetAppImage() { + return ("app-image".equals(targetFormat)); + } + + private static final Set multi_args = new TreeSet<>(Arrays.asList( + StandardBundlerParam.JAVA_OPTIONS.getID(), + StandardBundlerParam.ARGUMENTS.getID(), + StandardBundlerParam.MODULE_PATH.getID(), + StandardBundlerParam.ADD_MODULES.getID(), + StandardBundlerParam.LIMIT_MODULES.getID(), + StandardBundlerParam.FILE_ASSOCIATIONS.getID() + )); + + @SuppressWarnings("unchecked") + public void addBundleArgument(String key, Object value) { + // special hack for multi-line arguments + if (multi_args.contains(key)) { + Object existingValue = bundlerArguments.get(key); + if (existingValue instanceof String && value instanceof String) { + String delim = "\n\n"; + if (key.equals(StandardBundlerParam.MODULE_PATH.getID())) { + delim = File.pathSeparator; + } else if (key.equals( + StandardBundlerParam.ADD_MODULES.getID())) { + delim = ","; + } + bundlerArguments.put(key, existingValue + delim + value); + } else if (existingValue instanceof List && value instanceof List) { + ((List)existingValue).addAll((List)value); + } else if (existingValue instanceof Map && + value instanceof String && ((String)value).contains("=")) { + String[] mapValues = ((String)value).split("=", 2); + ((Map)existingValue).put(mapValues[0], mapValues[1]); + } else { + bundlerArguments.put(key, value); + } + } else { + bundlerArguments.put(key, value); + } + } + + BundleParams getBundleParams() { + BundleParams bundleParams = new BundleParams(); + + // construct app resources relative to destination folder! + bundleParams.setAppResourcesList(resources); + + Map unescapedHtmlParams = new TreeMap<>(); + Map escapedHtmlParams = new TreeMap<>(); + + // check for collisions + TreeSet keys = new TreeSet<>(bundlerArguments.keySet()); + keys.retainAll(bundleParams.getBundleParamsAsMap().keySet()); + + if (!keys.isEmpty()) { + throw new RuntimeException("Deploy Params and Bundler Arguments " + + "overlap in the following values:" + keys.toString()); + } + + bundleParams.addAllBundleParams(bundlerArguments); + + return bundleParams; + } + + @Override + public String toString() { + return "DeployParams {" + "output: " + outdir + + " resources: {" + resources + "}}"; + } + +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/DottedVersion.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/DottedVersion.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/DottedVersion.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Dotted numeric version string. + * E.g.: 1.0.37, 10, 0.5 + */ +class DottedVersion implements Comparable { + + public DottedVersion(String version) { + greedy = true; + components = parseVersionString(version, greedy); + value = version; + } + + private DottedVersion(String version, boolean greedy) { + this.greedy = greedy; + components = parseVersionString(version, greedy); + value = version; + } + + public static DottedVersion greedy(String version) { + return new DottedVersion(version); + } + + public static DottedVersion lazy(String version) { + return new DottedVersion(version, false); + } + + @Override + public int compareTo(String o) { + int result = 0; + int[] otherComponents = parseVersionString(o, greedy); + for (int i = 0; i < Math.min(components.length, otherComponents.length) + && result == 0; ++i) { + result = components[i] - otherComponents[i]; + } + + if (result == 0) { + result = components.length - otherComponents.length; + } + + return result; + } + + private static int[] parseVersionString(String version, boolean greedy) { + Objects.requireNonNull(version); + if (version.isEmpty()) { + if (!greedy) { + return new int[] {0}; + } + throw new IllegalArgumentException("Version may not be empty string"); + } + + int lastNotZeroIdx = -1; + List components = new ArrayList<>(); + for (var component : version.split("\\.", -1)) { + if (component.isEmpty()) { + if (!greedy) { + break; + } + + throw new IllegalArgumentException(String.format( + "Version [%s] contains a zero lenght component", version)); + } + + if (!DIGITS.matcher(component).matches()) { + // Catch "+N" and "-N" cases. + if (!greedy) { + break; + } + + throw new IllegalArgumentException(String.format( + "Version [%s] contains invalid component [%s]", version, + component)); + } + + final int num; + try { + num = Integer.parseInt(component); + } catch (NumberFormatException ex) { + if (!greedy) { + break; + } + + throw ex; + } + + if (num != 0) { + lastNotZeroIdx = components.size(); + } + components.add(num); + } + + if (lastNotZeroIdx + 1 != components.size()) { + // Strip trailing zeros. + components = components.subList(0, lastNotZeroIdx + 1); + } + + if (components.isEmpty()) { + components.add(0); + } + return components.stream().mapToInt(Integer::intValue).toArray(); + } + + @Override + public String toString() { + return value; + } + + final private int[] components; + final private String value; + final private boolean greedy; + + private static final Pattern DIGITS = Pattern.compile("\\d+"); +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Executor.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Executor.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Executor.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +final public class Executor { + + Executor() { + } + + Executor setOutputConsumer(Consumer> v) { + outputConsumer = v; + return this; + } + + Executor saveOutput(boolean v) { + saveOutput = v; + return this; + } + + Executor setProcessBuilder(ProcessBuilder v) { + pb = v; + return this; + } + + Executor setCommandLine(String... cmdline) { + return setProcessBuilder(new ProcessBuilder(cmdline)); + } + + List getOutput() { + return output; + } + + Executor executeExpectSuccess() throws IOException { + int ret = execute(); + if (0 != ret) { + throw new IOException( + String.format("Command %s exited with %d code", + createLogMessage(pb), ret)); + } + return this; + } + + int execute() throws IOException { + output = null; + + boolean needProcessOutput = outputConsumer != null || Log.isVerbose() || saveOutput; + if (needProcessOutput) { + pb.redirectErrorStream(true); + } else { + // We are not going to read process output, so need to notify + // ProcessBuilder about this. Otherwise some processes might just + // hang up (`ldconfig -p`). + pb.redirectError(ProcessBuilder.Redirect.DISCARD); + pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); + } + + Log.verbose(String.format("Running %s", createLogMessage(pb))); + Process p = pb.start(); + + if (needProcessOutput) { + try (var br = new BufferedReader(new InputStreamReader( + p.getInputStream()))) { + final List savedOutput; + // Need to save output if explicitely requested (saveOutput=true) or + // if will be used used by multiple consumers + if ((outputConsumer != null && Log.isVerbose()) || saveOutput) { + savedOutput = br.lines().collect(Collectors.toList()); + if (saveOutput) { + output = savedOutput; + } + } else { + savedOutput = null; + } + + Supplier> outputStream = () -> { + if (savedOutput != null) { + return savedOutput.stream(); + } + return br.lines(); + }; + + if (Log.isVerbose()) { + outputStream.get().forEach(Log::verbose); + } + + if (outputConsumer != null) { + outputConsumer.accept(outputStream.get()); + } + + if (savedOutput == null) { + // For some processes on Linux if the output stream + // of the process is opened but not consumed, the process + // would exit with code 141. + // It turned out that reading just a single line of process + // output fixes the problem, but let's process + // all of the output, just in case. + br.lines().forEach(x -> {}); + } + } + } + + try { + return p.waitFor(); + } catch (InterruptedException ex) { + Log.verbose(ex); + throw new RuntimeException(ex); + } + } + + static Executor of(String... cmdline) { + return new Executor().setCommandLine(cmdline); + } + + static Executor of(ProcessBuilder pb) { + return new Executor().setProcessBuilder(pb); + } + + private static String createLogMessage(ProcessBuilder pb) { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%s", pb.command())); + if (pb.directory() != null) { + sb.append(String.format("in %s", pb.directory().getAbsolutePath())); + } + return sb.toString(); + } + + private ProcessBuilder pb; + private boolean saveOutput; + private List output; + private Consumer> outputConsumer; +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/FileAssociation.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/FileAssociation.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/FileAssociation.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + +final class FileAssociation { + void verify() { + if (extensions.isEmpty()) { + Log.error(I18N.getString( + "message.creating-association-with-null-extension")); + } + } + + static List fetchFrom(Map params) { + String launcherName = APP_NAME.fetchFrom(params); + + return FILE_ASSOCIATIONS.fetchFrom(params).stream().filter( + Objects::nonNull).map(fa -> { + FileAssociation assoc = new FileAssociation(); + + assoc.launcherPath = Path.of(launcherName); + assoc.description = FA_DESCRIPTION.fetchFrom(fa); + assoc.extensions = Optional.ofNullable( + FA_EXTENSIONS.fetchFrom(fa)).orElse(Collections.emptyList()); + assoc.mimeTypes = Optional.ofNullable( + FA_CONTENT_TYPE.fetchFrom(fa)).orElse(Collections.emptyList()); + + File icon = FA_ICON.fetchFrom(fa); + if (icon != null) { + assoc.iconPath = icon.toPath(); + } + + return assoc; + }).collect(Collectors.toList()); + } + + Path launcherPath; + Path iconPath; + List mimeTypes; + List extensions; + String description; +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/I18N.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/I18N.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/I18N.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.util.ResourceBundle; + +class I18N { + + static String getString(String key) { + if (PLATFORM.containsKey(key)) { + return PLATFORM.getString(key); + } + return SHARED.getString(key); + } + + private static final ResourceBundle SHARED = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MainResources"); + + private static final ResourceBundle PLATFORM; + + static { + if (Platform.isLinux()) { + PLATFORM = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.LinuxResources"); + } else if (Platform.isWindows()) { + PLATFORM = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.WinResources"); + } else if (Platform.isMac()) { + PLATFORM = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MacResources"); + } else { + throw new IllegalStateException("Unknwon platform"); + } + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/IOUtils.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/IOUtils.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/IOUtils.java @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.*; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.nio.channels.FileChannel; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +/** + * IOUtils + * + * A collection of static utility methods. + */ +public class IOUtils { + + public static void deleteRecursive(File path) throws IOException { + if (!path.exists()) { + return; + } + Path directory = path.toPath(); + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attr) throws IOException { + if (Platform.getPlatform() == Platform.WINDOWS) { + Files.setAttribute(file, "dos:readonly", false); + } + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, + BasicFileAttributes attr) throws IOException { + if (Platform.getPlatform() == Platform.WINDOWS) { + Files.setAttribute(dir, "dos:readonly", false); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) + throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + public static void copyRecursive(Path src, Path dest) throws IOException { + copyRecursive(src, dest, List.of()); + } + + public static void copyRecursive(Path src, Path dest, + final List excludes) throws IOException { + Files.walkFileTree(src, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(final Path dir, + final BasicFileAttributes attrs) throws IOException { + if (excludes.contains(dir.toFile().getName())) { + return FileVisitResult.SKIP_SUBTREE; + } else { + Files.createDirectories(dest.resolve(src.relativize(dir))); + return FileVisitResult.CONTINUE; + } + } + + @Override + public FileVisitResult visitFile(final Path file, + final BasicFileAttributes attrs) throws IOException { + if (!excludes.contains(file.toFile().getName())) { + Files.copy(file, dest.resolve(src.relativize(file))); + } + return FileVisitResult.CONTINUE; + } + }); + } + + public static void copyFile(File sourceFile, File destFile) + throws IOException { + destFile.getParentFile().mkdirs(); + + //recreate the file as existing copy may have weird permissions + destFile.delete(); + destFile.createNewFile(); + + try (FileChannel source = new FileInputStream(sourceFile).getChannel(); + FileChannel destination = + new FileOutputStream(destFile).getChannel()) { + + if (destination != null && source != null) { + destination.transferFrom(source, 0, source.size()); + } + } + + //preserve executable bit! + if (sourceFile.canExecute()) { + destFile.setExecutable(true, false); + } + if (!sourceFile.canWrite()) { + destFile.setReadOnly(); + } + destFile.setReadable(true, false); + } + + // run "launcher paramfile" in the directory where paramfile is kept + public static void run(String launcher, File paramFile) + throws IOException { + if (paramFile != null && paramFile.exists()) { + ProcessBuilder pb = + new ProcessBuilder(launcher, paramFile.getName()); + pb = pb.directory(paramFile.getParentFile()); + exec(pb); + } + } + + public static void exec(ProcessBuilder pb) + throws IOException { + exec(pb, false, null); + } + + static void exec(ProcessBuilder pb, boolean testForPresenseOnly, + PrintStream consumer) throws IOException { + List output = new ArrayList<>(); + Executor exec = Executor.of(pb).setOutputConsumer(lines -> { + lines.forEach(output::add); + if (consumer != null) { + output.forEach(consumer::println); + } + }); + + if (testForPresenseOnly) { + exec.execute(); + } else { + exec.executeExpectSuccess(); + } + } + + public static int getProcessOutput(List result, String... args) + throws IOException, InterruptedException { + + ProcessBuilder pb = new ProcessBuilder(args); + + final Process p = pb.start(); + + List list = new ArrayList<>(); + + final BufferedReader in = + new BufferedReader(new InputStreamReader(p.getInputStream())); + final BufferedReader err = + new BufferedReader(new InputStreamReader(p.getErrorStream())); + + Thread t = new Thread(() -> { + try { + String line; + while ((line = in.readLine()) != null) { + list.add(line); + } + } catch (IOException ioe) { + Log.verbose(ioe); + } + + try { + String line; + while ((line = err.readLine()) != null) { + Log.error(line); + } + } catch (IOException ioe) { + Log.verbose(ioe); + } + }); + t.setDaemon(true); + t.start(); + + int ret = p.waitFor(); + + result.clear(); + result.addAll(list); + + return ret; + } + + static void writableOutputDir(Path outdir) throws PackagerException { + File file = outdir.toFile(); + + if (!file.isDirectory() && !file.mkdirs()) { + throw new PackagerException("error.cannot-create-output-dir", + file.getAbsolutePath()); + } + if (!file.canWrite()) { + throw new PackagerException("error.cannot-write-to-output-dir", + file.getAbsolutePath()); + } + } + + public static Path replaceSuffix(Path path, String suffix) { + Path parent = path.getParent(); + String filename = path.getFileName().toString().replaceAll("\\.[^.]*$", "") + + Optional.ofNullable(suffix).orElse(""); + return parent != null ? parent.resolve(filename) : Path.of(filename); + } + + public static Path addSuffix(Path path, String suffix) { + Path parent = path.getParent(); + String filename = path.getFileName().toString() + suffix; + return parent != null ? parent.resolve(filename) : Path.of(filename); + } + + public static String getSuffix(Path path) { + String filename = replaceSuffix(path.getFileName(), null).toString(); + return path.getFileName().toString().substring(filename.length()); + } + + @FunctionalInterface + public static interface XmlConsumer { + void accept(XMLStreamWriter xml) throws IOException, XMLStreamException; + } + + public static void createXml(Path dstFile, XmlConsumer xmlConsumer) throws + IOException { + XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance(); + try (Writer w = Files.newBufferedWriter(dstFile)) { + // Wrap with pretty print proxy + XMLStreamWriter xml = (XMLStreamWriter) Proxy.newProxyInstance( + XMLStreamWriter.class.getClassLoader(), new Class[]{ + XMLStreamWriter.class}, new PrettyPrintHandler( + xmlFactory.createXMLStreamWriter(w))); + + xml.writeStartDocument(); + xmlConsumer.accept(xml); + xml.writeEndDocument(); + xml.flush(); + xml.close(); + } catch (XMLStreamException ex) { + throw new IOException(ex); + } catch (IOException ex) { + throw ex; + } + } + + private static class PrettyPrintHandler implements InvocationHandler { + + PrettyPrintHandler(XMLStreamWriter target) { + this.target = target; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws + Throwable { + switch (method.getName()) { + case "writeStartElement": + // update state of parent node + if (depth > 0) { + hasChildElement.put(depth - 1, true); + } + // reset state of current node + hasChildElement.put(depth, false); + // indent for current depth + target.writeCharacters(EOL); + target.writeCharacters(repeat(depth, INDENT)); + depth++; + break; + case "writeEndElement": + depth--; + if (hasChildElement.get(depth) == true) { + target.writeCharacters(EOL); + target.writeCharacters(repeat(depth, INDENT)); + } + break; + case "writeProcessingInstruction": + case "writeEmptyElement": + // update state of parent node + if (depth > 0) { + hasChildElement.put(depth - 1, true); + } + // indent for current depth + target.writeCharacters(EOL); + target.writeCharacters(repeat(depth, INDENT)); + break; + default: + break; + } + method.invoke(target, args); + return null; + } + + private static String repeat(int d, String s) { + StringBuilder sb = new StringBuilder(); + while (d-- > 0) { + sb.append(s); + } + return sb.toString(); + } + + private final XMLStreamWriter target; + private int depth = 0; + private final Map hasChildElement = new HashMap<>(); + private static final String INDENT = " "; + private static final String EOL = "\n"; + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/JLinkBundlerHelper.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/JLinkBundlerHelper.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/JLinkBundlerHelper.java @@ -0,0 +1,398 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.Optional; +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.regex.Matcher; +import java.util.spi.ToolProvider; +import java.util.jar.JarFile; +import java.lang.module.Configuration; +import java.lang.module.ResolvedModule; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleFinder; +import java.lang.module.ModuleReference; +import jdk.internal.module.ModulePath; + + +final class JLinkBundlerHelper { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MainResources"); + + static final ToolProvider JLINK_TOOL = + ToolProvider.findFirst("jlink").orElseThrow(); + + static File getMainJar(Map params) { + File result = null; + RelativeFileSet fileset = + StandardBundlerParam.MAIN_JAR.fetchFrom(params); + + if (fileset != null) { + String filename = fileset.getIncludedFiles().iterator().next(); + result = fileset.getBaseDirectory().toPath(). + resolve(filename).toFile(); + + if (result == null || !result.exists()) { + String srcdir = + StandardBundlerParam.SOURCE_DIR.fetchFrom(params); + + if (srcdir != null) { + result = new File(srcdir + File.separator + filename); + } + } + } + + return result; + } + + static String getMainClassFromModule(Map params) { + String mainModule = StandardBundlerParam.MODULE.fetchFrom(params); + if (mainModule != null) { + + int index = mainModule.indexOf("/"); + if (index > 0) { + return mainModule.substring(index + 1); + } else { + ModuleDescriptor descriptor = + JLinkBundlerHelper.getMainModuleDescription(params); + if (descriptor != null) { + Optional mainClass = descriptor.mainClass(); + if (mainClass.isPresent()) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.module-class"), + mainClass.get(), + JLinkBundlerHelper.getMainModule(params))); + return mainClass.get(); + } + } + } + } + return null; + } + + static String getMainModule(Map params) { + String result = null; + String mainModule = StandardBundlerParam.MODULE.fetchFrom(params); + + if (mainModule != null) { + int index = mainModule.indexOf("/"); + + if (index > 0) { + result = mainModule.substring(0, index); + } else { + result = mainModule; + } + } + + return result; + } + + static void execute(Map params, + AbstractAppImageBuilder imageBuilder) + throws IOException, Exception { + + List modulePath = + StandardBundlerParam.MODULE_PATH.fetchFrom(params); + Set addModules = + StandardBundlerParam.ADD_MODULES.fetchFrom(params); + Set limitModules = + StandardBundlerParam.LIMIT_MODULES.fetchFrom(params); + Path outputDir = imageBuilder.getRuntimeRoot(); + File mainJar = getMainJar(params); + ModFile.ModType mainJarType = ModFile.ModType.Unknown; + + if (mainJar != null) { + mainJarType = new ModFile(mainJar).getModType(); + } else if (StandardBundlerParam.MODULE.fetchFrom(params) == null) { + // user specified only main class, all jars will be on the classpath + mainJarType = ModFile.ModType.UnnamedJar; + } + + boolean bindServices = + StandardBundlerParam.BIND_SERVICES.fetchFrom(params); + + // Modules + String mainModule = getMainModule(params); + if (mainModule == null) { + if (mainJarType == ModFile.ModType.UnnamedJar) { + if (addModules.isEmpty()) { + // The default for an unnamed jar is ALL_DEFAULT + addModules.add(ModuleHelper.ALL_DEFAULT); + } + } else if (mainJarType == ModFile.ModType.Unknown || + mainJarType == ModFile.ModType.ModularJar) { + addModules.add(ModuleHelper.ALL_DEFAULT); + } + } + + Set modules = new ModuleHelper( + modulePath, addModules, limitModules).modules(); + + if (mainModule != null) { + modules.add(mainModule); + } + + runJLink(outputDir, modulePath, modules, limitModules, + new HashMap(), bindServices); + + imageBuilder.prepareApplicationFiles(params); + } + + + // Returns the path to the JDK modules in the user defined module path. + static Path findPathOfModule( List modulePath, String moduleName) { + + for (Path path : modulePath) { + Path moduleNamePath = path.resolve(moduleName); + + if (Files.exists(moduleNamePath)) { + return path; + } + } + + return null; + } + + static ModuleDescriptor getMainModuleDescription(Map params) { + boolean hasModule = params.containsKey(StandardBundlerParam.MODULE.getID()); + if (hasModule) { + List modulePath = StandardBundlerParam.MODULE_PATH.fetchFrom(params); + if (!modulePath.isEmpty()) { + ModuleFinder finder = ModuleFinder.of(modulePath.toArray(new Path[0])); + String mainModule = JLinkBundlerHelper.getMainModule(params); + Optional omref = finder.find(mainModule); + if (omref.isPresent()) { + return omref.get().descriptor(); + } + } + } + + return null; + } + + /* + * Returns the set of modules that would be visible by default for + * a non-modular-aware application consisting of the given elements. + */ + private static Set getDefaultModules( + Collection paths, Collection addModules) { + + // the modules in the run-time image that export an API + Stream systemRoots = ModuleFinder.ofSystem().findAll().stream() + .map(ModuleReference::descriptor) + .filter(JLinkBundlerHelper::exportsAPI) + .map(ModuleDescriptor::name); + + Set roots = Stream.concat(systemRoots, + addModules.stream()).collect(Collectors.toSet()); + + ModuleFinder finder = createModuleFinder(paths); + + return Configuration.empty() + .resolveAndBind(finder, ModuleFinder.of(), roots) + .modules() + .stream() + .map(ResolvedModule::name) + .collect(Collectors.toSet()); + } + + /* + * Returns true if the given module exports an API to all module. + */ + private static boolean exportsAPI(ModuleDescriptor descriptor) { + return descriptor.exports() + .stream() + .anyMatch(e -> !e.isQualified()); + } + + private static ModuleFinder createModuleFinder(Collection modulePath) { + return ModuleFinder.compose( + ModulePath.of(JarFile.runtimeVersion(), true, + modulePath.toArray(Path[]::new)), + ModuleFinder.ofSystem()); + } + + private static class ModuleHelper { + // The token for "all modules on the module path". + private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH"; + + // The token for "all valid runtime modules". + static final String ALL_DEFAULT = "ALL-DEFAULT"; + + private final Set modules = new HashSet<>(); + ModuleHelper(List paths, Set addModules, + Set limitModules) { + boolean addAllModulePath = false; + boolean addDefaultMods = false; + + for (Iterator iterator = addModules.iterator(); + iterator.hasNext();) { + String module = iterator.next(); + + switch (module) { + case ALL_MODULE_PATH: + iterator.remove(); + addAllModulePath = true; + break; + case ALL_DEFAULT: + iterator.remove(); + addDefaultMods = true; + break; + default: + this.modules.add(module); + } + } + + if (addAllModulePath) { + this.modules.addAll(getModuleNamesFromPath(paths)); + } else if (addDefaultMods) { + this.modules.addAll(getDefaultModules( + paths, addModules)); + } + } + + Set modules() { + return modules; + } + + private static Set getModuleNamesFromPath(List paths) { + + return createModuleFinder(paths) + .findAll() + .stream() + .map(ModuleReference::descriptor) + .map(ModuleDescriptor::name) + .collect(Collectors.toSet()); + } + } + + private static void runJLink(Path output, List modulePath, + Set modules, Set limitModules, + HashMap user, boolean bindServices) + throws PackagerException { + + // This is just to ensure jlink is given a non-existant directory + // The passed in output path should be non-existant or empty directory + try { + IOUtils.deleteRecursive(output.toFile()); + } catch (IOException ioe) { + throw new PackagerException(ioe); + } + + ArrayList args = new ArrayList(); + args.add("--output"); + args.add(output.toString()); + if (modulePath != null && !modulePath.isEmpty()) { + args.add("--module-path"); + args.add(getPathList(modulePath)); + } + if (modules != null && !modules.isEmpty()) { + args.add("--add-modules"); + args.add(getStringList(modules)); + } + if (limitModules != null && !limitModules.isEmpty()) { + args.add("--limit-modules"); + args.add(getStringList(limitModules)); + } + if (user != null && !user.isEmpty()) { + for (Map.Entry entry : user.entrySet()) { + args.add(entry.getKey()); + args.add(entry.getValue()); + } + } else { + args.add("--strip-native-commands"); + args.add("--strip-debug"); + args.add("--no-man-pages"); + args.add("--no-header-files"); + if (bindServices) { + args.add("--bind-services"); + } + } + + StringWriter writer = new StringWriter(); + PrintWriter pw = new PrintWriter(writer); + + Log.verbose("jlink arguments: " + args); + int retVal = JLINK_TOOL.run(pw, pw, args.toArray(new String[0])); + String jlinkOut = writer.toString(); + + if (retVal != 0) { + throw new PackagerException("error.jlink.failed" , jlinkOut); + } else if (jlinkOut.length() > 0) { + Log.verbose("jlink output: " + jlinkOut); + } + } + + private static String getPathList(List pathList) { + String ret = null; + for (Path p : pathList) { + String s = Matcher.quoteReplacement(p.toString()); + if (ret == null) { + ret = s; + } else { + ret += File.pathSeparator + s; + } + } + return ret; + } + + private static String getStringList(Set strings) { + String ret = null; + for (String s : strings) { + if (ret == null) { + ret = s; + } else { + ret += "," + s; + } + } + return (ret == null) ? null : Matcher.quoteReplacement(ret); + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/JPackageToolProvider.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/JPackageToolProvider.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/JPackageToolProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.PrintWriter; +import java.util.spi.ToolProvider; + +/** + * JPackageToolProvider + * + * This is the ToolProvider implementation exported + * to java.util.spi.ToolProvider and ultimately javax.tools.ToolProvider + */ +public class JPackageToolProvider implements ToolProvider { + + public String name() { + return "jpackage"; + } + + public synchronized int run( + PrintWriter out, PrintWriter err, String... args) { + try { + return new jdk.incubator.jpackage.main.Main().execute(out, err, args); + } catch (RuntimeException re) { + Log.error(re.getMessage()); + Log.verbose(re); + return 1; + } + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Log.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Log.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Log.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2011, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * Log + * + * General purpose logging mechanism. + */ +public class Log { + public static class Logger { + private boolean verbose = false; + private PrintWriter out = null; + private PrintWriter err = null; + + // verbose defaults to true unless environment variable JPACKAGE_DEBUG + // is set to true. + // Then it is only set to true by using --verbose jpackage option + + public Logger() { + verbose = ("true".equals(System.getenv("JPACKAGE_DEBUG"))); + } + + public void setVerbose() { + verbose = true; + } + + public boolean isVerbose() { + return verbose; + } + + public void setPrintWriter(PrintWriter out, PrintWriter err) { + this.out = out; + this.err = err; + } + + public void flush() { + if (out != null) { + out.flush(); + } + + if (err != null) { + err.flush(); + } + } + + public void info(String msg) { + if (out != null) { + out.println(msg); + } else { + System.out.println(msg); + } + } + + public void error(String msg) { + if (err != null) { + err.println(msg); + } else { + System.err.println(msg); + } + } + + public void verbose(Throwable t) { + if (out != null && verbose) { + t.printStackTrace(out); + } else if (verbose) { + t.printStackTrace(System.out); + } + } + + public void verbose(String msg) { + if (out != null && verbose) { + out.println(msg); + } else if (verbose) { + System.out.println(msg); + } + } + } + + private static Logger delegate = null; + + public static void setLogger(Logger logger) { + delegate = (logger != null) ? logger : new Logger(); + } + + public static void flush() { + if (delegate != null) { + delegate.flush(); + } + } + + public static void info(String msg) { + if (delegate != null) { + delegate.info(msg); + } + } + + public static void error(String msg) { + if (delegate != null) { + delegate.error(msg); + } + } + + public static void setVerbose() { + if (delegate != null) { + delegate.setVerbose(); + } + } + + public static boolean isVerbose() { + return (delegate != null) ? delegate.isVerbose() : false; + } + + public static void verbose(String msg) { + if (delegate != null) { + delegate.verbose(msg); + } + } + + public static void verbose(Throwable t) { + if (delegate != null) { + delegate.verbose(t); + } + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ModFile.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ModFile.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ModFile.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +final class ModFile { + private final String filename; + private final ModType moduleType; + + enum JarType {All, UnnamedJar, ModularJar} + enum ModType { + Unknown, UnnamedJar, ModularJar, Jmod, ExplodedModule} + + ModFile(File aFile) { + super(); + filename = aFile.getPath(); + moduleType = getModType(aFile); + } + + String getModName() { + File file = new File(getFileName()); + // do not try to remove extension for directories + return moduleType == ModType.ExplodedModule ? + file.getName() : getFileWithoutExtension(file.getName()); + } + + String getFileName() { + return filename; + } + + ModType getModType() { + return moduleType; + } + + private static ModType getModType(File aFile) { + ModType result = ModType.Unknown; + String filename = aFile.getAbsolutePath(); + + if (aFile.isFile()) { + if (filename.endsWith(".jmod")) { + result = ModType.Jmod; + } + else if (filename.endsWith(".jar")) { + JarType status = isModularJar(filename); + + if (status == JarType.ModularJar) { + result = ModType.ModularJar; + } + else if (status == JarType.UnnamedJar) { + result = ModType.UnnamedJar; + } + } + } + else if (aFile.isDirectory()) { + File moduleInfo = new File( + filename + File.separator + "module-info.class"); + + if (moduleInfo.exists()) { + result = ModType.ExplodedModule; + } + } + + return result; + } + + private static JarType isModularJar(String FileName) { + JarType result = JarType.All; + + try (ZipInputStream zip = + new ZipInputStream(new FileInputStream(FileName))) { + result = JarType.UnnamedJar; + + for (ZipEntry entry = zip.getNextEntry(); entry != null; + entry = zip.getNextEntry()) { + if (entry.getName().matches("module-info.class")) { + result = JarType.ModularJar; + break; + } + } + } catch (IOException ex) { + } + + return result; + } + + private static String getFileWithoutExtension(String FileName) { + return FileName.replaceFirst("[.][^.]+$", ""); + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/OverridableResource.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/OverridableResource.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/OverridableResource.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import static jdk.incubator.jpackage.internal.StandardBundlerParam.RESOURCE_DIR; +import jdk.incubator.jpackage.internal.resources.ResourceLocator; + +/** + * Resource file that may have the default value supplied by jpackage. It can be + * overridden by a file from resource directory set with {@code --resource-dir} + * jpackage parameter. + * + * Resource has default name and public name. Default name is the name of a file + * in {@code jdk.incubator.jpackage.internal.resources} package that provides the default + * value of the resource. + * + * Public name is a path relative to resource directory to a file with custom + * value of the resource. + * + * Use #setPublicName to set the public name. + * + * If #setPublicName was not called, name of file passed in #saveToFile function + * will be used as a public name. + * + * Use #setExternal to set arbitrary file as a source of resource. If non-null + * value was passed in #setExternal call that value will be used as a path to file + * to copy in the destination file passed in #saveToFile function call. + */ +final class OverridableResource { + + OverridableResource(String defaultName) { + this.defaultName = defaultName; + } + + OverridableResource setSubstitutionData(Map v) { + if (v != null) { + // Disconnect `v` + substitutionData = new HashMap<>(v); + } else { + substitutionData = null; + } + return this; + } + + OverridableResource setCategory(String v) { + category = v; + return this; + } + + OverridableResource setResourceDir(Path v) { + resourceDir = v; + return this; + } + + OverridableResource setResourceDir(File v) { + return setResourceDir(toPath(v)); + } + + /** + * Set name of file to look for in resource dir. + * + * @return this + */ + OverridableResource setPublicName(Path v) { + publicName = v; + return this; + } + + OverridableResource setPublicName(String v) { + return setPublicName(Path.of(v)); + } + + OverridableResource setExternal(Path v) { + externalPath = v; + return this; + } + + OverridableResource setExternal(File v) { + return setExternal(toPath(v)); + } + + void saveToFile(Path dest) throws IOException { + final String printableCategory; + if (category != null) { + printableCategory = String.format("[%s]", category); + } else { + printableCategory = ""; + } + + if (externalPath != null && externalPath.toFile().exists()) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.using-custom-resource-from-file"), + printableCategory, + externalPath.toAbsolutePath().normalize())); + + try (InputStream in = Files.newInputStream(externalPath)) { + processResourceStream(in, dest); + } + return; + } + + final Path resourceName = Optional.ofNullable(publicName).orElse( + dest.getFileName()); + + if (resourceDir != null) { + final Path customResource = resourceDir.resolve(resourceName); + if (customResource.toFile().exists()) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.using-custom-resource"), printableCategory, + resourceDir.normalize().toAbsolutePath().relativize( + customResource.normalize().toAbsolutePath()))); + + try (InputStream in = Files.newInputStream(customResource)) { + processResourceStream(in, dest); + } + return; + } + } + + if (defaultName != null) { + Log.verbose(MessageFormat.format( + I18N.getString("message.using-default-resource"), + defaultName, printableCategory, resourceName)); + + try (InputStream in = readDefault(defaultName)) { + processResourceStream(in, dest); + } + } + } + + void saveToFile(File dest) throws IOException { + saveToFile(dest.toPath()); + } + + static InputStream readDefault(String resourceName) { + return ResourceLocator.class.getResourceAsStream(resourceName); + } + + static OverridableResource createResource(String defaultName, + Map params) { + return new OverridableResource(defaultName).setResourceDir( + RESOURCE_DIR.fetchFrom(params)); + } + + private static List substitute(Stream lines, + Map substitutionData) { + return lines.map(line -> { + String result = line; + for (var entry : substitutionData.entrySet()) { + result = result.replace(entry.getKey(), Optional.ofNullable( + entry.getValue()).orElse("")); + } + return result; + }).collect(Collectors.toList()); + } + + private static Path toPath(File v) { + if (v != null) { + return v.toPath(); + } + return null; + } + + private void processResourceStream(InputStream rawResource, Path dest) + throws IOException { + if (substitutionData == null) { + Files.createDirectories(dest.getParent()); + Files.copy(rawResource, dest, StandardCopyOption.REPLACE_EXISTING); + } else { + // Utf8 in and out + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(rawResource, StandardCharsets.UTF_8))) { + Files.createDirectories(dest.getParent()); + Files.write(dest, substitute(reader.lines(), substitutionData)); + } + } + } + + private Map substitutionData; + private String category; + private Path resourceDir; + private Path publicName; + private Path externalPath; + private final String defaultName; +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/PackagerException.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/PackagerException.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/PackagerException.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2011, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.text.MessageFormat; +import java.util.ResourceBundle; + +public class PackagerException extends Exception { + private static final long serialVersionUID = 1L; + private static final ResourceBundle bundle = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MainResources"); + + public PackagerException(Throwable cause) { + super(cause); + } + + public PackagerException(String key, Throwable cause) { + super(bundle.getString(key), cause); + } + + public PackagerException(String key) { + super(bundle.getString(key)); + } + + public PackagerException(String key, String ... arguments) { + super(MessageFormat.format( + bundle.getString(key), (Object[]) arguments)); + } + + public PackagerException( + Throwable cause, String key, String ... arguments) { + super(MessageFormat.format(bundle.getString(key), + (Object[]) arguments), cause); + } + +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/PathGroup.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/PathGroup.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/PathGroup.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +/** + * Group of paths. + * Each path in the group is assigned a unique id. + */ +final class PathGroup { + PathGroup(Map paths) { + entries = new HashMap<>(paths); + } + + Path getPath(Object id) { + if (id == null) { + throw new NullPointerException(); + } + return entries.get(id); + } + + void setPath(Object id, Path path) { + if (path != null) { + entries.put(id, path); + } else { + entries.remove(id); + } + } + + /** + * All configured entries. + */ + List paths() { + return entries.values().stream().collect(Collectors.toList()); + } + + /** + * Root entries. + */ + List roots() { + // Sort by the number of path components in ascending order. + List> sorted = normalizedPaths().stream().sorted( + (a, b) -> a.getKey().getNameCount() - b.getKey().getNameCount()).collect( + Collectors.toList()); + + // Returns `true` if `a` is a parent of `b` + BiFunction, Map.Entry, Boolean> isParentOrSelf = (a, b) -> { + return a == b || b.getKey().startsWith(a.getKey()); + }; + + return sorted.stream().filter( + v -> v == sorted.stream().sequential().filter( + v2 -> isParentOrSelf.apply(v2, v)).findFirst().get()).map( + v -> v.getValue()).collect(Collectors.toList()); + } + + long sizeInBytes() throws IOException { + long reply = 0; + for (Path dir : roots().stream().filter(f -> Files.isDirectory(f)).collect( + Collectors.toList())) { + try (Stream stream = Files.walk(dir)) { + reply += stream.filter(p -> Files.isRegularFile(p)).mapToLong( + f -> f.toFile().length()).sum(); + } + } + return reply; + } + + PathGroup resolveAt(Path root) { + return new PathGroup(entries.entrySet().stream().collect( + Collectors.toMap(e -> e.getKey(), + e -> root.resolve(e.getValue())))); + } + + void copy(PathGroup dst) throws IOException { + copy(this, dst, null, false); + } + + void move(PathGroup dst) throws IOException { + copy(this, dst, null, true); + } + + void transform(PathGroup dst, TransformHandler handler) throws IOException { + copy(this, dst, handler, false); + } + + static interface Facade { + PathGroup pathGroup(); + + default Collection paths() { + return pathGroup().paths(); + } + + default List roots() { + return pathGroup().roots(); + } + + default long sizeInBytes() throws IOException { + return pathGroup().sizeInBytes(); + } + + T resolveAt(Path root); + + default void copy(Facade dst) throws IOException { + pathGroup().copy(dst.pathGroup()); + } + + default void move(Facade dst) throws IOException { + pathGroup().move(dst.pathGroup()); + } + + default void transform(Facade dst, TransformHandler handler) throws + IOException { + pathGroup().transform(dst.pathGroup(), handler); + } + } + + static interface TransformHandler { + public void copyFile(Path src, Path dst) throws IOException; + public void createDirectory(Path dir) throws IOException; + } + + private static void copy(PathGroup src, PathGroup dst, + TransformHandler handler, boolean move) throws IOException { + List> copyItems = new ArrayList<>(); + List excludeItems = new ArrayList<>(); + + for (var id: src.entries.keySet()) { + Path srcPath = src.entries.get(id); + if (dst.entries.containsKey(id)) { + copyItems.add(Map.entry(srcPath, dst.entries.get(id))); + } else { + excludeItems.add(srcPath); + } + } + + copy(move, copyItems, excludeItems, handler); + } + + private static void copy(boolean move, List> entries, + List excludePaths, TransformHandler handler) throws + IOException { + + if (handler == null) { + handler = new TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + Files.createDirectories(dst.getParent()); + if (move) { + Files.move(src, dst); + } else { + Files.copy(src, dst); + } + } + + @Override + public void createDirectory(Path dir) throws IOException { + Files.createDirectories(dir); + } + }; + } + + // destination -> source file mapping + Map actions = new HashMap<>(); + for (var action: entries) { + Path src = action.getKey(); + Path dst = action.getValue(); + if (src.toFile().isDirectory()) { + try (Stream stream = Files.walk(src)) { + stream.sequential().forEach(path -> actions.put(dst.resolve( + src.relativize(path)).normalize(), path)); + } + } else { + actions.put(dst.normalize(), src); + } + } + + for (var action : actions.entrySet()) { + Path dst = action.getKey(); + Path src = action.getValue(); + + if (excludePaths.stream().anyMatch(src::startsWith)) { + continue; + } + + if (src.equals(dst) || !src.toFile().exists()) { + continue; + } + + if (src.toFile().isDirectory()) { + handler.createDirectory(dst); + } else { + handler.copyFile(src, dst); + } + } + + if (move) { + // Delete source dirs. + for (var entry: entries) { + File srcFile = entry.getKey().toFile(); + if (srcFile.isDirectory()) { + IOUtils.deleteRecursive(srcFile); + } + } + } + } + + private static Map.Entry normalizedPath(Path v) { + final Path normalized; + if (!v.isAbsolute()) { + normalized = Path.of("./").resolve(v.normalize()); + } else { + normalized = v.normalize(); + } + + return Map.entry(normalized, v); + } + + private List> normalizedPaths() { + return entries.values().stream().map(PathGroup::normalizedPath).collect( + Collectors.toList()); + } + + private final Map entries; +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Platform.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Platform.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Platform.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.util.regex.Pattern; + +/** + * Platform + * + * Use Platform to detect the operating system + * that is currently running. + * + * Example: + * + * Platform platform = Platform.getPlatform(); + * + * switch(platform) { + * case Platform.MAC: { + * // Do something + * break; + * } + * case Platform.WINDOWS: + * case Platform.LINUX: { + * // Do something else + * } + * } + * + */ +enum Platform {UNKNOWN, WINDOWS, LINUX, MAC; + private static final Platform platform; + private static final int majorVersion; + private static final int minorVersion; + + static { + String os = System.getProperty("os.name").toLowerCase(); + + if (os.indexOf("win") >= 0) { + platform = Platform.WINDOWS; + } + else if (os.indexOf("nix") >= 0 || os.indexOf("nux") >= 0) { + platform = Platform.LINUX; + } + else if (os.indexOf("mac") >= 0) { + platform = Platform.MAC; + } + else { + platform = Platform.UNKNOWN; + } + + String version = System.getProperty("os.version").toString(); + String[] parts = version.split(Pattern.quote(".")); + + if (parts.length > 0) { + majorVersion = Integer.parseInt(parts[0]); + + if (parts.length > 1) { + minorVersion = Integer.parseInt(parts[1]); + } + else { + minorVersion = -1; + } + } + else { + majorVersion = -1; + minorVersion = -1; + } + } + + private Platform() {} + + static Platform getPlatform() { + return platform; + } + + static int getMajorVersion() { + return majorVersion; + } + + static int getMinorVersion() { + return minorVersion; + } + + static boolean isWindows() { + return getPlatform() == WINDOWS; + } + + static boolean isMac() { + return getPlatform() == MAC; + } + + static boolean isLinux() { + return getPlatform() == LINUX; + } + + static RuntimeException throwUnknownPlatformError() { + throw new IllegalArgumentException("Unknown platform"); + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/PlatformPackage.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/PlatformPackage.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/PlatformPackage.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.nio.file.Path; + +/** + * + * Platform package of an application. + */ +interface PlatformPackage { + /** + * Platform-specific package name. + */ + String name(); + + /** + * Root directory where sources for packaging tool should be stored + */ + Path sourceRoot(); + + /** + * Source application layout from which to build the package. + */ + ApplicationLayout sourceApplicationLayout(); + + /** + * Application layout of the installed package. + */ + ApplicationLayout installedApplicationLayout(); +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/RelativeFileSet.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/RelativeFileSet.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/RelativeFileSet.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * RelativeFileSet + * + * A class encapsulating a directory and a set of files within it. + */ +class RelativeFileSet { + + private File basedir; + private Set files = new LinkedHashSet<>(); + + RelativeFileSet(File base, Collection files) { + basedir = base; + String baseAbsolute = basedir.getAbsolutePath(); + for (File f: files) { + String absolute = f.getAbsolutePath(); + if (!absolute.startsWith(baseAbsolute)) { + throw new RuntimeException("File " + f.getAbsolutePath() + + " does not belong to " + baseAbsolute); + } + if (!absolute.equals(baseAbsolute)) { + // possible in jpackage case + this.files.add(absolute.substring(baseAbsolute.length()+1)); + } + } + } + + RelativeFileSet(File base, Set files) { + this(base, (Collection) files); + } + + File getBaseDirectory() { + return basedir; + } + + Set getIncludedFiles() { + return files; + } + + @Override + public String toString() { + if (files.size() == 1) { + return "" + basedir + File.pathSeparator + files; + } + return "RelativeFileSet {basedir:" + basedir + + ", files: {" + files + "}"; + } + +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ScriptRunner.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ScriptRunner.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ScriptRunner.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import static jdk.incubator.jpackage.internal.OverridableResource.createResource; +import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME; +import static jdk.incubator.jpackage.internal.StandardBundlerParam.CONFIG_ROOT; + +/** + * Runs custom script from resource directory. + */ +class ScriptRunner { + ScriptRunner() { + environment = new ProcessBuilder().environment(); + } + + ScriptRunner setResourceCategoryId(String v) { + resourceCategoryId = v; + return this; + } + + ScriptRunner setDirectory(Path v) { + directory = v; + return this; + } + + ScriptRunner setScriptNameSuffix(String v) { + scriptNameSuffix = v; + return this; + } + + ScriptRunner addEnvironment(Map v) { + environment.putAll(v); + return this; + } + + ScriptRunner setEnvironmentVariable(String envVarName, String envVarValue) { + Objects.requireNonNull(envVarName); + if (envVarValue == null) { + environment.remove(envVarName); + } else { + environment.put(envVarName, envVarValue); + } + return this; + } + + public void run(Map params) throws IOException { + String scriptName = String.format("%s-%s%s", APP_NAME.fetchFrom(params), + scriptNameSuffix, scriptSuffix()); + Path scriptPath = CONFIG_ROOT.fetchFrom(params).toPath().resolve( + scriptName); + createResource(null, params) + .setCategory(I18N.getString(resourceCategoryId)) + .saveToFile(scriptPath); + if (!Files.exists(scriptPath)) { + return; + } + + ProcessBuilder pb = new ProcessBuilder(shell(), + scriptPath.toAbsolutePath().toString()); + Map workEnvironment = pb.environment(); + workEnvironment.clear(); + workEnvironment.putAll(environment); + + if (directory != null) { + pb.directory(directory.toFile()); + } + + Executor.of(pb).executeExpectSuccess(); + } + + private static String shell() { + if (Platform.isWindows()) { + return "cscript"; + } + return Optional.ofNullable(System.getenv("SHELL")).orElseGet(() -> "sh"); + } + + private static String scriptSuffix() { + if (Platform.isWindows()) { + return ".wsf"; + } + return ".sh"; + } + + private String scriptNameSuffix; + private String resourceCategoryId; + private Path directory; + private Map environment; +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/StandardBundlerParam.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/StandardBundlerParam.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/StandardBundlerParam.java @@ -0,0 +1,790 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.IOException; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleDescriptor.Version; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.HashSet; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * StandardBundlerParam + * + * A parameter to a bundler. + * + * Also contains static definitions of all of the common bundler parameters. + * (additional platform specific and mode specific bundler parameters + * are defined in each of the specific bundlers) + * + * Also contains static methods that operate on maps of parameters. + */ +class StandardBundlerParam extends BundlerParamInfo { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MainResources"); + private static final String JAVABASEJMOD = "java.base.jmod"; + private final static String DEFAULT_VERSION = "1.0"; + private final static String DEFAULT_RELEASE = "1"; + + StandardBundlerParam(String id, Class valueType, + Function, T> defaultValueFunction, + BiFunction, T> stringConverter) + { + this.id = id; + this.valueType = valueType; + this.defaultValueFunction = defaultValueFunction; + this.stringConverter = stringConverter; + } + + static final StandardBundlerParam APP_RESOURCES = + new StandardBundlerParam<>( + BundleParams.PARAM_APP_RESOURCES, + RelativeFileSet.class, + null, // no default. Required parameter + null // no string translation, + // tool must provide complex type + ); + + @SuppressWarnings("unchecked") + static final + StandardBundlerParam> APP_RESOURCES_LIST = + new StandardBundlerParam<>( + BundleParams.PARAM_APP_RESOURCES + "List", + (Class>) (Object) List.class, + // Default is appResources, as a single item list + p -> new ArrayList<>(Collections.singletonList( + APP_RESOURCES.fetchFrom(p))), + StandardBundlerParam::createAppResourcesListFromString + ); + + static final StandardBundlerParam SOURCE_DIR = + new StandardBundlerParam<>( + Arguments.CLIOptions.INPUT.getId(), + String.class, + p -> null, + (s, p) -> { + String value = String.valueOf(s); + if (value.charAt(value.length() - 1) == + File.separatorChar) { + return value.substring(0, value.length() - 1); + } + else { + return value; + } + } + ); + + // note that each bundler is likely to replace this one with + // their own converter + static final StandardBundlerParam MAIN_JAR = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAIN_JAR.getId(), + RelativeFileSet.class, + params -> { + extractMainClassInfoFromAppResources(params); + return (RelativeFileSet) params.get("mainJar"); + }, + (s, p) -> getMainJar(s, p) + ); + + static final StandardBundlerParam CLASSPATH = + new StandardBundlerParam<>( + "classpath", + String.class, + params -> { + extractMainClassInfoFromAppResources(params); + String cp = (String) params.get("classpath"); + return cp == null ? "" : cp; + }, + (s, p) -> s + ); + + static final StandardBundlerParam MAIN_CLASS = + new StandardBundlerParam<>( + Arguments.CLIOptions.APPCLASS.getId(), + String.class, + params -> { + if (isRuntimeInstaller(params)) { + return null; + } + extractMainClassInfoFromAppResources(params); + String s = (String) params.get( + BundleParams.PARAM_APPLICATION_CLASS); + if (s == null) { + s = JLinkBundlerHelper.getMainClassFromModule( + params); + } + return s; + }, + (s, p) -> s + ); + + static final StandardBundlerParam PREDEFINED_RUNTIME_IMAGE = + new StandardBundlerParam<>( + Arguments.CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId(), + File.class, + params -> null, + (s, p) -> new File(s) + ); + + static final StandardBundlerParam APP_NAME = + new StandardBundlerParam<>( + Arguments.CLIOptions.NAME.getId(), + String.class, + params -> { + String s = MAIN_CLASS.fetchFrom(params); + if (s != null) { + int idx = s.lastIndexOf("."); + if (idx >= 0) { + return s.substring(idx+1); + } + return s; + } else if (isRuntimeInstaller(params)) { + File f = PREDEFINED_RUNTIME_IMAGE.fetchFrom(params); + if (f != null) { + return f.getName(); + } + } + return null; + }, + (s, p) -> s + ); + + static final StandardBundlerParam ICON = + new StandardBundlerParam<>( + Arguments.CLIOptions.ICON.getId(), + File.class, + params -> null, + (s, p) -> new File(s) + ); + + static final StandardBundlerParam VENDOR = + new StandardBundlerParam<>( + Arguments.CLIOptions.VENDOR.getId(), + String.class, + params -> I18N.getString("param.vendor.default"), + (s, p) -> s + ); + + static final StandardBundlerParam DESCRIPTION = + new StandardBundlerParam<>( + Arguments.CLIOptions.DESCRIPTION.getId(), + String.class, + params -> params.containsKey(APP_NAME.getID()) + ? APP_NAME.fetchFrom(params) + : I18N.getString("param.description.default"), + (s, p) -> s + ); + + static final StandardBundlerParam COPYRIGHT = + new StandardBundlerParam<>( + Arguments.CLIOptions.COPYRIGHT.getId(), + String.class, + params -> MessageFormat.format(I18N.getString( + "param.copyright.default"), new Date()), + (s, p) -> s + ); + + @SuppressWarnings("unchecked") + static final StandardBundlerParam> ARGUMENTS = + new StandardBundlerParam<>( + Arguments.CLIOptions.ARGUMENTS.getId(), + (Class>) (Object) List.class, + params -> Collections.emptyList(), + (s, p) -> null + ); + + @SuppressWarnings("unchecked") + static final StandardBundlerParam> JAVA_OPTIONS = + new StandardBundlerParam<>( + Arguments.CLIOptions.JAVA_OPTIONS.getId(), + (Class>) (Object) List.class, + params -> Collections.emptyList(), + (s, p) -> Arrays.asList(s.split("\n\n")) + ); + + // note that each bundler is likely to replace this one with + // their own converter + static final StandardBundlerParam VERSION = + new StandardBundlerParam<>( + Arguments.CLIOptions.VERSION.getId(), + String.class, + params -> getDefaultAppVersion(params), + (s, p) -> s + ); + + static final StandardBundlerParam RELEASE = + new StandardBundlerParam<>( + Arguments.CLIOptions.RELEASE.getId(), + String.class, + params -> DEFAULT_RELEASE, + (s, p) -> s + ); + + @SuppressWarnings("unchecked") + public static final StandardBundlerParam LICENSE_FILE = + new StandardBundlerParam<>( + Arguments.CLIOptions.LICENSE_FILE.getId(), + String.class, + params -> null, + (s, p) -> s + ); + + static final StandardBundlerParam TEMP_ROOT = + new StandardBundlerParam<>( + Arguments.CLIOptions.TEMP_ROOT.getId(), + File.class, + params -> { + try { + return Files.createTempDirectory( + "jdk.incubator.jpackage").toFile(); + } catch (IOException ioe) { + return null; + } + }, + (s, p) -> new File(s) + ); + + public static final StandardBundlerParam CONFIG_ROOT = + new StandardBundlerParam<>( + "configRoot", + File.class, + params -> { + File root = + new File(TEMP_ROOT.fetchFrom(params), "config"); + root.mkdirs(); + return root; + }, + (s, p) -> null + ); + + static final StandardBundlerParam IDENTIFIER = + new StandardBundlerParam<>( + "identifier.default", + String.class, + params -> { + String s = MAIN_CLASS.fetchFrom(params); + if (s == null) return null; + + int idx = s.lastIndexOf("."); + if (idx >= 1) { + return s.substring(0, idx); + } + return s; + }, + (s, p) -> s + ); + + static final StandardBundlerParam BIND_SERVICES = + new StandardBundlerParam<>( + Arguments.CLIOptions.BIND_SERVICES.getId(), + Boolean.class, + params -> false, + (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? + true : Boolean.valueOf(s) + ); + + + static final StandardBundlerParam VERBOSE = + new StandardBundlerParam<>( + Arguments.CLIOptions.VERBOSE.getId(), + Boolean.class, + params -> false, + // valueOf(null) is false, and we actually do want null + (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? + true : Boolean.valueOf(s) + ); + + static final StandardBundlerParam RESOURCE_DIR = + new StandardBundlerParam<>( + Arguments.CLIOptions.RESOURCE_DIR.getId(), + File.class, + params -> null, + (s, p) -> new File(s) + ); + + static final BundlerParamInfo INSTALL_DIR = + new StandardBundlerParam<>( + Arguments.CLIOptions.INSTALL_DIR.getId(), + String.class, + params -> null, + (s, p) -> s + ); + + static final StandardBundlerParam PREDEFINED_APP_IMAGE = + new StandardBundlerParam<>( + Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId(), + File.class, + params -> null, + (s, p) -> new File(s)); + + @SuppressWarnings("unchecked") + static final StandardBundlerParam>> ADD_LAUNCHERS = + new StandardBundlerParam<>( + Arguments.CLIOptions.ADD_LAUNCHER.getId(), + (Class>>) (Object) + List.class, + params -> new ArrayList<>(1), + // valueOf(null) is false, and we actually do want null + (s, p) -> null + ); + + @SuppressWarnings("unchecked") + static final StandardBundlerParam + >> FILE_ASSOCIATIONS = + new StandardBundlerParam<>( + Arguments.CLIOptions.FILE_ASSOCIATIONS.getId(), + (Class>>) (Object) + List.class, + params -> new ArrayList<>(1), + // valueOf(null) is false, and we actually do want null + (s, p) -> null + ); + + @SuppressWarnings("unchecked") + static final StandardBundlerParam> FA_EXTENSIONS = + new StandardBundlerParam<>( + "fileAssociation.extension", + (Class>) (Object) List.class, + params -> null, // null means not matched to an extension + (s, p) -> Arrays.asList(s.split("(,|\\s)+")) + ); + + @SuppressWarnings("unchecked") + static final StandardBundlerParam> FA_CONTENT_TYPE = + new StandardBundlerParam<>( + "fileAssociation.contentType", + (Class>) (Object) List.class, + params -> null, + // null means not matched to a content/mime type + (s, p) -> Arrays.asList(s.split("(,|\\s)+")) + ); + + static final StandardBundlerParam FA_DESCRIPTION = + new StandardBundlerParam<>( + "fileAssociation.description", + String.class, + params -> APP_NAME.fetchFrom(params) + " File", + null + ); + + static final StandardBundlerParam FA_ICON = + new StandardBundlerParam<>( + "fileAssociation.icon", + File.class, + ICON::fetchFrom, + (s, p) -> new File(s) + ); + + @SuppressWarnings("unchecked") + static final BundlerParamInfo> MODULE_PATH = + new StandardBundlerParam<>( + Arguments.CLIOptions.MODULE_PATH.getId(), + (Class>) (Object)List.class, + p -> { return getDefaultModulePath(); }, + (s, p) -> { + List modulePath = Arrays.asList(s + .split(File.pathSeparator)).stream() + .map(ss -> new File(ss).toPath()) + .collect(Collectors.toList()); + Path javaBasePath = null; + if (modulePath != null) { + javaBasePath = JLinkBundlerHelper + .findPathOfModule(modulePath, JAVABASEJMOD); + } else { + modulePath = new ArrayList(); + } + + // Add the default JDK module path to the module path. + if (javaBasePath == null) { + List jdkModulePath = getDefaultModulePath(); + + if (jdkModulePath != null) { + modulePath.addAll(jdkModulePath); + javaBasePath = + JLinkBundlerHelper.findPathOfModule( + modulePath, JAVABASEJMOD); + } + } + + if (javaBasePath == null || + !Files.exists(javaBasePath)) { + Log.error(String.format(I18N.getString( + "warning.no.jdk.modules.found"))); + } + + return modulePath; + }); + + static final BundlerParamInfo MODULE = + new StandardBundlerParam<>( + Arguments.CLIOptions.MODULE.getId(), + String.class, + p -> null, + (s, p) -> { + return String.valueOf(s); + }); + + @SuppressWarnings("unchecked") + static final BundlerParamInfo> ADD_MODULES = + new StandardBundlerParam<>( + Arguments.CLIOptions.ADD_MODULES.getId(), + (Class>) (Object) Set.class, + p -> new LinkedHashSet(), + (s, p) -> new LinkedHashSet<>(Arrays.asList(s.split(","))) + ); + + @SuppressWarnings("unchecked") + static final BundlerParamInfo> LIMIT_MODULES = + new StandardBundlerParam<>( + "limit-modules", + (Class>) (Object) Set.class, + p -> new LinkedHashSet(), + (s, p) -> new LinkedHashSet<>(Arrays.asList(s.split(","))) + ); + + static boolean isRuntimeInstaller(Map params) { + if (params.containsKey(MODULE.getID()) || + params.containsKey(MAIN_JAR.getID()) || + params.containsKey(PREDEFINED_APP_IMAGE.getID())) { + return false; // we are building or are given an application + } + // runtime installer requires --runtime-image, if this is false + // here then we should have thrown error validating args. + return params.containsKey(PREDEFINED_RUNTIME_IMAGE.getID()); + } + + static File getPredefinedAppImage(Map params) { + File applicationImage = null; + if (PREDEFINED_APP_IMAGE.fetchFrom(params) != null) { + applicationImage = PREDEFINED_APP_IMAGE.fetchFrom(params); + if (!applicationImage.exists()) { + throw new RuntimeException( + MessageFormat.format(I18N.getString( + "message.app-image-dir-does-not-exist"), + PREDEFINED_APP_IMAGE.getID(), + applicationImage.toString())); + } + } + return applicationImage; + } + + static void copyPredefinedRuntimeImage( + Map params, + AbstractAppImageBuilder appBuilder) + throws IOException , ConfigException { + File topImage = PREDEFINED_RUNTIME_IMAGE.fetchFrom(params); + if (!topImage.exists()) { + throw new ConfigException( + MessageFormat.format(I18N.getString( + "message.runtime-image-dir-does-not-exist"), + PREDEFINED_RUNTIME_IMAGE.getID(), + topImage.toString()), + MessageFormat.format(I18N.getString( + "message.runtime-image-dir-does-not-exist.advice"), + PREDEFINED_RUNTIME_IMAGE.getID())); + } + File image = appBuilder.getRuntimeImageDir(topImage); + // copy whole runtime, need to skip jmods and src.zip + final List excludes = Arrays.asList("jmods", "src.zip"); + IOUtils.copyRecursive(image.toPath(), appBuilder.getRuntimeRoot(), excludes); + + // if module-path given - copy modules to appDir/mods + List modulePath = + StandardBundlerParam.MODULE_PATH.fetchFrom(params); + List defaultModulePath = getDefaultModulePath(); + Path dest = appBuilder.getAppModsDir(); + + if (dest != null) { + for (Path mp : modulePath) { + if (!defaultModulePath.contains(mp)) { + Files.createDirectories(dest); + IOUtils.copyRecursive(mp, dest); + } + } + } + + appBuilder.prepareApplicationFiles(params); + } + + static void extractMainClassInfoFromAppResources( + Map params) { + boolean hasMainClass = params.containsKey(MAIN_CLASS.getID()); + boolean hasMainJar = params.containsKey(MAIN_JAR.getID()); + boolean hasMainJarClassPath = params.containsKey(CLASSPATH.getID()); + boolean hasModule = params.containsKey(MODULE.getID()); + + if (hasMainClass && hasMainJar && hasMainJarClassPath || hasModule || + isRuntimeInstaller(params)) { + return; + } + + // it's a pair. + // The [0] is the srcdir [1] is the file relative to sourcedir + List filesToCheck = new ArrayList<>(); + + if (hasMainJar) { + RelativeFileSet rfs = MAIN_JAR.fetchFrom(params); + for (String s : rfs.getIncludedFiles()) { + filesToCheck.add( + new String[] {rfs.getBaseDirectory().toString(), s}); + } + } else if (hasMainJarClassPath) { + for (String s : CLASSPATH.fetchFrom(params).split("\\s+")) { + if (APP_RESOURCES.fetchFrom(params) != null) { + filesToCheck.add( + new String[] {APP_RESOURCES.fetchFrom(params) + .getBaseDirectory().toString(), s}); + } + } + } else { + List rfsl = APP_RESOURCES_LIST.fetchFrom(params); + if (rfsl == null || rfsl.isEmpty()) { + return; + } + for (RelativeFileSet rfs : rfsl) { + if (rfs == null) continue; + + for (String s : rfs.getIncludedFiles()) { + filesToCheck.add( + new String[]{rfs.getBaseDirectory().toString(), s}); + } + } + } + + // presume the set iterates in-order + for (String[] fnames : filesToCheck) { + try { + // only sniff jars + if (!fnames[1].toLowerCase().endsWith(".jar")) continue; + + File file = new File(fnames[0], fnames[1]); + // that actually exist + if (!file.exists()) continue; + + try (JarFile jf = new JarFile(file)) { + Manifest m = jf.getManifest(); + Attributes attrs = (m != null) ? + m.getMainAttributes() : null; + + if (attrs != null) { + if (!hasMainJar) { + if (fnames[0] == null) { + fnames[0] = file.getParentFile().toString(); + } + params.put(MAIN_JAR.getID(), new RelativeFileSet( + new File(fnames[0]), + new LinkedHashSet<>(Collections + .singletonList(file)))); + } + if (!hasMainJarClassPath) { + String cp = + attrs.getValue(Attributes.Name.CLASS_PATH); + params.put(CLASSPATH.getID(), + cp == null ? "" : cp); + } + break; + } + } + } catch (IOException ignore) { + ignore.printStackTrace(); + } + } + } + + static void validateMainClassInfoFromAppResources( + Map params) throws ConfigException { + boolean hasMainClass = params.containsKey(MAIN_CLASS.getID()); + boolean hasMainJar = params.containsKey(MAIN_JAR.getID()); + boolean hasMainJarClassPath = params.containsKey(CLASSPATH.getID()); + boolean hasModule = params.containsKey(MODULE.getID()); + boolean hasAppImage = params.containsKey(PREDEFINED_APP_IMAGE.getID()); + + if (hasMainClass && hasMainJar && hasMainJarClassPath || + hasAppImage || isRuntimeInstaller(params)) { + return; + } + if (hasModule) { + if (JLinkBundlerHelper.getMainClassFromModule(params) == null) { + throw new ConfigException( + I18N.getString("ERR_NoMainClass"), null); + } + } else { + extractMainClassInfoFromAppResources(params); + + if (!params.containsKey(MAIN_CLASS.getID())) { + if (hasMainJar) { + throw new ConfigException( + MessageFormat.format(I18N.getString( + "error.no-main-class-with-main-jar"), + MAIN_JAR.fetchFrom(params)), + MessageFormat.format(I18N.getString( + "error.no-main-class-with-main-jar.advice"), + MAIN_JAR.fetchFrom(params))); + } else { + throw new ConfigException( + I18N.getString("error.no-main-class"), + I18N.getString("error.no-main-class.advice")); + } + } + } + } + + private static List + createAppResourcesListFromString(String s, + Map objectObjectMap) { + List result = new ArrayList<>(); + for (String path : s.split("[:;]")) { + File f = new File(path); + if (f.getName().equals("*") || path.endsWith("/") || + path.endsWith("\\")) { + if (f.getName().equals("*")) { + f = f.getParentFile(); + } + Set theFiles = new HashSet<>(); + try { + try (Stream stream = Files.walk(f.toPath())) { + stream.filter(Files::isRegularFile) + .forEach(p -> theFiles.add(p.toFile())); + } + } catch (IOException e) { + e.printStackTrace(); + } + result.add(new RelativeFileSet(f, theFiles)); + } else { + result.add(new RelativeFileSet(f.getParentFile(), + Collections.singleton(f))); + } + } + return result; + } + + private static RelativeFileSet getMainJar( + String mainJarValue, Map params) { + for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(params)) { + File appResourcesRoot = rfs.getBaseDirectory(); + File mainJarFile = new File(appResourcesRoot, mainJarValue); + + if (mainJarFile.exists()) { + return new RelativeFileSet(appResourcesRoot, + new LinkedHashSet<>(Collections.singletonList( + mainJarFile))); + } + mainJarFile = new File(mainJarValue); + if (mainJarFile.exists()) { + // absolute path for main-jar may fail is not legal + // below contains explicit error message. + } else { + List modulePath = MODULE_PATH.fetchFrom(params); + modulePath.removeAll(getDefaultModulePath()); + if (!modulePath.isEmpty()) { + Path modularJarPath = JLinkBundlerHelper.findPathOfModule( + modulePath, mainJarValue); + if (modularJarPath != null && + Files.exists(modularJarPath)) { + return new RelativeFileSet(appResourcesRoot, + new LinkedHashSet<>(Collections.singletonList( + modularJarPath.toFile()))); + } + } + } + } + + throw new IllegalArgumentException( + new ConfigException(MessageFormat.format(I18N.getString( + "error.main-jar-does-not-exist"), + mainJarValue), I18N.getString( + "error.main-jar-does-not-exist.advice"))); + } + + static List getDefaultModulePath() { + List result = new ArrayList(); + Path jdkModulePath = Paths.get( + System.getProperty("java.home"), "jmods").toAbsolutePath(); + + if (jdkModulePath != null && Files.exists(jdkModulePath)) { + result.add(jdkModulePath); + } + else { + // On a developer build the JDK Home isn't where we expect it + // relative to the jmods directory. Do some extra + // processing to find it. + Map env = System.getenv(); + + if (env.containsKey("JDK_HOME")) { + jdkModulePath = Paths.get(env.get("JDK_HOME"), + ".." + File.separator + "images" + + File.separator + "jmods").toAbsolutePath(); + + if (jdkModulePath != null && Files.exists(jdkModulePath)) { + result.add(jdkModulePath); + } + } + } + + return result; + } + + static String getDefaultAppVersion(Map params) { + String appVersion = DEFAULT_VERSION; + + ModuleDescriptor descriptor = JLinkBundlerHelper.getMainModuleDescription(params); + if (descriptor != null) { + Optional oversion = descriptor.version(); + if (oversion.isPresent()) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.module-version"), + oversion.get().toString(), + JLinkBundlerHelper.getMainModule(params))); + appVersion = oversion.get().toString(); + } + } + + return appVersion; + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ToolValidator.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ToolValidator.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ToolValidator.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.IOException; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Stream; + + +public final class ToolValidator { + + ToolValidator(String tool) { + this(Path.of(tool)); + } + + ToolValidator(Path toolPath) { + this.toolPath = toolPath; + args = new ArrayList<>(); + + if (Platform.getPlatform() == Platform.LINUX) { + setCommandLine("--version"); + } + + setToolNotFoundErrorHandler(null); + setToolOldVersionErrorHandler(null); + } + + ToolValidator setCommandLine(String... args) { + this.args = List.of(args); + return this; + } + + ToolValidator setMinimalVersion(Comparable v) { + minimalVersion = v; + return this; + } + + ToolValidator setVersionParser(Function, String> v) { + versionParser = v; + return this; + } + + ToolValidator setToolNotFoundErrorHandler( + BiFunction v) { + toolNotFoundErrorHandler = v; + return this; + } + + ToolValidator setToolOldVersionErrorHandler(BiFunction v) { + toolOldVersionErrorHandler = v; + return this; + } + + ConfigException validate() { + List cmdline = new ArrayList<>(); + cmdline.add(toolPath.toString()); + cmdline.addAll(args); + + String name = toolPath.getFileName().toString(); + try { + ProcessBuilder pb = new ProcessBuilder(cmdline); + AtomicBoolean canUseTool = new AtomicBoolean(); + if (minimalVersion == null) { + // No version check. + canUseTool.setPlain(true); + } + + String[] version = new String[1]; + Executor.of(pb).setOutputConsumer(lines -> { + if (versionParser != null && minimalVersion != null) { + version[0] = versionParser.apply(lines); + if (minimalVersion.compareTo(version[0]) < 0) { + canUseTool.setPlain(true); + } + } + }).execute(); + + if (!canUseTool.getPlain()) { + if (toolOldVersionErrorHandler != null) { + return toolOldVersionErrorHandler.apply(name, version[0]); + } + return new ConfigException(MessageFormat.format(I18N.getString( + "error.tool-old-version"), name, minimalVersion), + MessageFormat.format(I18N.getString( + "error.tool-old-version.advice"), name, + minimalVersion)); + } + } catch (IOException e) { + if (toolNotFoundErrorHandler != null) { + return toolNotFoundErrorHandler.apply(name, e); + } + return new ConfigException(MessageFormat.format(I18N.getString( + "error.tool-not-found"), name, e.getMessage()), + MessageFormat.format(I18N.getString( + "error.tool-not-found.advice"), name), e); + } + + // All good. Tool can be used. + return null; + } + + private final Path toolPath; + private List args; + private Comparable minimalVersion; + private Function, String> versionParser; + private BiFunction toolNotFoundErrorHandler; + private BiFunction toolOldVersionErrorHandler; +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ValidOptions.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ValidOptions.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ValidOptions.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import jdk.incubator.jpackage.internal.Arguments.CLIOptions; + +/** + * ValidOptions + * + * Two basic methods for validating command line options. + * + * initArgs() + * Computes the Map of valid options for each mode on this Platform. + * + * checkIfSupported(CLIOptions arg) + * Determine if the given arg is valid on this platform. + * + * checkIfImageSupported(CLIOptions arg) + * Determine if the given arg is valid for creating app image. + * + * checkIfInstallerSupported(CLIOptions arg) + * Determine if the given arg is valid for creating installer. + * + */ +class ValidOptions { + + enum USE { + ALL, // valid in all cases + LAUNCHER, // valid when creating a launcher + INSTALL // valid when creating an installer + } + + private static final HashMap options = new HashMap<>(); + + + // initializing list of mandatory arguments + static { + options.put(CLIOptions.NAME.getId(), USE.ALL); + options.put(CLIOptions.VERSION.getId(), USE.ALL); + options.put(CLIOptions.OUTPUT.getId(), USE.ALL); + options.put(CLIOptions.TEMP_ROOT.getId(), USE.ALL); + options.put(CLIOptions.VERBOSE.getId(), USE.ALL); + options.put(CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId(), USE.ALL); + options.put(CLIOptions.RESOURCE_DIR.getId(), USE.ALL); + options.put(CLIOptions.DESCRIPTION.getId(), USE.ALL); + options.put(CLIOptions.VENDOR.getId(), USE.ALL); + options.put(CLIOptions.COPYRIGHT.getId(), USE.ALL); + options.put(CLIOptions.PACKAGE_TYPE.getId(), USE.ALL); + + options.put(CLIOptions.INPUT.getId(), USE.LAUNCHER); + options.put(CLIOptions.MODULE.getId(), USE.LAUNCHER); + options.put(CLIOptions.MODULE_PATH.getId(), USE.LAUNCHER); + options.put(CLIOptions.ADD_MODULES.getId(), USE.LAUNCHER); + options.put(CLIOptions.MAIN_JAR.getId(), USE.LAUNCHER); + options.put(CLIOptions.APPCLASS.getId(), USE.LAUNCHER); + options.put(CLIOptions.ICON.getId(), USE.LAUNCHER); + options.put(CLIOptions.ARGUMENTS.getId(), USE.LAUNCHER); + options.put(CLIOptions.JAVA_OPTIONS.getId(), USE.LAUNCHER); + options.put(CLIOptions.ADD_LAUNCHER.getId(), USE.LAUNCHER); + options.put(CLIOptions.BIND_SERVICES.getId(), USE.LAUNCHER); + + options.put(CLIOptions.LICENSE_FILE.getId(), USE.INSTALL); + options.put(CLIOptions.INSTALL_DIR.getId(), USE.INSTALL); + options.put(CLIOptions.PREDEFINED_APP_IMAGE.getId(), USE.INSTALL); + + options.put(CLIOptions.FILE_ASSOCIATIONS.getId(), + (Platform.getPlatform() == Platform.MAC) ? USE.ALL : USE.INSTALL); + + if (Platform.getPlatform() == Platform.WINDOWS) { + options.put(CLIOptions.WIN_CONSOLE_HINT.getId(), USE.LAUNCHER); + + options.put(CLIOptions.WIN_MENU_HINT.getId(), USE.INSTALL); + options.put(CLIOptions.WIN_MENU_GROUP.getId(), USE.INSTALL); + options.put(CLIOptions.WIN_SHORTCUT_HINT.getId(), USE.INSTALL); + options.put(CLIOptions.WIN_DIR_CHOOSER.getId(), USE.INSTALL); + options.put(CLIOptions.WIN_UPGRADE_UUID.getId(), USE.INSTALL); + options.put(CLIOptions.WIN_PER_USER_INSTALLATION.getId(), + USE.INSTALL); + } + + if (Platform.getPlatform() == Platform.MAC) { + options.put(CLIOptions.MAC_SIGN.getId(), USE.ALL); + options.put(CLIOptions.MAC_BUNDLE_NAME.getId(), USE.ALL); + options.put(CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(), USE.ALL); + options.put(CLIOptions.MAC_BUNDLE_SIGNING_PREFIX.getId(), + USE.ALL); + options.put(CLIOptions.MAC_SIGNING_KEY_NAME.getId(), USE.ALL); + options.put(CLIOptions.MAC_SIGNING_KEYCHAIN.getId(), USE.ALL); + options.put(CLIOptions.MAC_APP_STORE_ENTITLEMENTS.getId(), + USE.ALL); + } + + if (Platform.getPlatform() == Platform.LINUX) { + options.put(CLIOptions.LINUX_BUNDLE_NAME.getId(), USE.INSTALL); + options.put(CLIOptions.LINUX_DEB_MAINTAINER.getId(), USE.INSTALL); + options.put(CLIOptions.LINUX_CATEGORY.getId(), USE.INSTALL); + options.put(CLIOptions.LINUX_RPM_LICENSE_TYPE.getId(), USE.INSTALL); + options.put(CLIOptions.LINUX_PACKAGE_DEPENDENCIES.getId(), + USE.INSTALL); + options.put(CLIOptions.LINUX_MENU_GROUP.getId(), USE.INSTALL); + options.put(CLIOptions.RELEASE.getId(), USE.INSTALL); + options.put(CLIOptions.LINUX_SHORTCUT_HINT.getId(), USE.INSTALL); + } + } + + static boolean checkIfSupported(CLIOptions arg) { + return options.containsKey(arg.getId()); + } + + static boolean checkIfImageSupported(CLIOptions arg) { + USE use = options.get(arg.getId()); + return USE.ALL == use || USE.LAUNCHER == use; + } + + static boolean checkIfInstallerSupported(CLIOptions arg) { + USE use = options.get(arg.getId()); + return USE.ALL == use || USE.INSTALL == use; + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/HelpResources.properties b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/HelpResources.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/HelpResources.properties @@ -0,0 +1,275 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +MSG_Help=Usage: jpackage \n\ +\n\ +Sample usages:\n\ +--------------\n\ +\ Generate an application package suitable for the host system:\n\ +\ For a modular application:\n\ +\ jpackage -n name -p modulePath -m moduleName/className\n\ +\ For a non-modular application:\n\ +\ jpackage -i inputDir -n name \\\n\ +\ --main-class className --main-jar myJar.jar\n\ +\ From a pre-built application image:\n\ +\ jpackage -n name --app-image appImageDir\n\ +\ Generate an application image:\n\ +\ For a modular application:\n\ +\ jpackage --type app-image -n name -p modulePath \\\n\ +\ -m moduleName/className\n\ +\ For a non-modular application:\n\ +\ jpackage --type app-image -i inputDir -n name \\\n\ +\ --main-class className --main-jar myJar.jar\n\ +\ To provide your own options to jlink, run jlink separately:\n\ +\ jlink --output appRuntimeImage -p modulePath -m moduleName \\\n\ +\ --no-header-files [...]\n\ +\ jpackage --type app-image -n name \\\n\ +\ -m moduleName/className --runtime-image appRuntimeImage\n\ +\ Generate a Java runtime package:\n\ +\ jpackage -n name --runtime-image \n\ +\n\ +Generic Options:\n\ +\ @ \n\ +\ Read options and/or mode from a file \n\ +\ This option can be used multiple times.\n\ +\ --type -t \n\ +\ The type of package to create\n\ +\ Valid values are: {1} \n\ +\ If this option is not specified a platform dependent\n\ +\ default type will be created.\n\ +\ --app-version \n\ +\ Version of the application and/or package\n\ +\ --copyright \n\ +\ Copyright for the application\n\ +\ --description \n\ +\ Description of the application\n\ +\ --help -h \n\ +\ Print the usage text with a list and description of each valid\n\ +\ option for the current platform to the output stream, and exit\n\ +\ --name -n \n\ +\ Name of the application and/or package\n\ +\ --dest -d \n\ +\ Path where generated output file is placed\n\ +\ Defaults to the current working directory.\n\ +\ (absolute path or relative to the current directory)\n\ +\ --temp \n\ +\ Path of a new or empty directory used to create temporary files\n\ +\ (absolute path or relative to the current directory)\n\ +\ If specified, the temp dir will not be removed upon the task\n\ +\ completion and must be removed manually\n\ +\ If not specified, a temporary directory will be created and\n\ +\ removed upon the task completion.\n\ +\ --vendor \n\ +\ Vendor of the application\n\ +\ --verbose\n\ +\ Enables verbose output\n\ +\ --version\n\ +\ Print the product version to the output stream and exit\n\ +\n\ +\Options for creating the runtime image:\n\ +\ --add-modules [,...]\n\ +\ A comma (",") separated list of modules to add.\n\ +\ This module list, along with the main module (if specified)\n\ +\ will be passed to jlink as the --add-module argument.\n\ +\ if not specified, either just the main module (if --module is\n\ +\ specified), or the default set of modules (if --main-jar is \n\ +\ specified) are used.\n\ +\ This option can be used multiple times.\n\ +\ --module-path -p ...\n\ +\ A {0} separated list of paths\n\ +\ Each path is either a directory of modules or the path to a\n\ +\ modular jar.\n\ +\ (each path is absolute or relative to the current directory)\n\ +\ This option can be used multiple times.\n\ +\ --bind-services \n\ +\ Pass on --bind-services option to jlink (which will link in \n\ +\ service provider modules and their dependences) \n\ +\ --runtime-image \n\ +\ Path of the predefined runtime image that will be copied into\n\ +\ the application image\n\ +\ (absolute path or relative to the current directory)\n\ +\ If --runtime-image is not specified, jpackage will run jlink to\n\ +\ create the runtime image using options:\n\ +\ --strip-debug, --no-header-files, --no-man-pages, and\n\ +\ --strip-native-commands.\n\ +\n\ +\Options for creating the application image:\n\ +\ --icon \n\ +\ Path of the icon of the application package\n\ +\ (absolute path or relative to the current directory)\n\ +\ --input -i \n\ +\ Path of the input directory that contains the files to be packaged\n\ +\ (absolute path or relative to the current directory)\n\ +\ All files in the input directory will be packaged into the\n\ +\ application image.\n\ +\n\ +\Options for creating the application launcher(s):\n\ +\ --add-launcher =\n\ +\ Name of launcher, and a path to a Properties file that contains\n\ +\ a list of key, value pairs\n\ +\ (absolute path or relative to the current directory)\n\ +\ The keys "module", "main-jar", "main-class",\n\ +\ "arguments", "java-options", "app-version", "icon", and\n\ +\ "win-console" can be used.\n\ +\ These options are added to, or used to overwrite, the original\n\ +\ command line options to build an additional alternative launcher.\n\ +\ The main application launcher will be built from the command line\n\ +\ options. Additional alternative launchers can be built using\n\ +\ this option, and this option can be used multiple times to\n\ +\ build multiple additional launchers. \n\ +\ --arguments
\n\ +\ Command line arguments to pass to the main class if no command\n\ +\ line arguments are given to the launcher\n\ +\ This option can be used multiple times.\n\ +\ --java-options \n\ +\ Options to pass to the Java runtime\n\ +\ This option can be used multiple times.\n\ +\ --main-class \n\ +\ Qualified name of the application main class to execute\n\ +\ This option can only be used if --main-jar is specified.\n\ +\ --main-jar
\n\ +\ The main JAR of the application; containing the main class\n\ +\ (specified as a path relative to the input path)\n\ +\ Either --module or --main-jar option can be specified but not\n\ +\ both.\n\ +\ --module -m [/
]\n\ +\ The main module (and optionally main class) of the application\n\ +\ This module must be located on the module path.\n\ +\ When this option is specified, the main module will be linked\n\ +\ in the Java runtime image. Either --module or --main-jar\n\ +\ option can be specified but not both.\n\ +{2}\n\ +\Options for creating the application package:\n\ +\ --app-image \n\ +\ Location of the predefined application image that is used\n\ +\ to build an installable package\n\ +\ (absolute path or relative to the current directory)\n\ +\ --file-associations \n\ +\ Path to a Properties file that contains list of key, value pairs\n\ +\ (absolute path or relative to the current directory)\n\ +\ The keys "extension", "mime-type", "icon", and "description"\n\ +\ can be used to describe the association.\n\ +\ This option can be used multiple times.\n\ +\ --install-dir \n\ +\ {4}\ +\ --license-file \n\ +\ Path to the license file\n\ +\ (absolute path or relative to the current directory)\n\ +\ --resource-dir \n\ +\ Path to override jpackage resources\n\ +\ Icons, template files, and other resources of jpackage can be\n\ +\ over-ridden by adding replacement resources to this directory.\n\ +\ (absolute path or relative to the current directory)\n\ +\ --runtime-image \n\ +\ Path of the predefined runtime image to install\n\ +\ (absolute path or relative to the current directory)\n\ +\ Option is required when creating a runtime package.\n\ +\n\ +\Platform dependent options for creating the application package:\n\ +{3} + +MSG_Help_win_launcher=\ +\n\ +\Platform dependent option for creating the application launcher:\n\ +\ --win-console\n\ +\ Creates a console launcher for the application, should be\n\ +\ specified for application which requires console interactions\n\ + +MSG_Help_win_install=\ +\ --win-dir-chooser\n\ +\ Adds a dialog to enable the user to choose a directory in which\n\ +\ the product is installed\n\ +\ --win-menu\n\ +\ Adds the application to the system menu\n\ +\ --win-menu-group \n\ +\ Start Menu group this application is placed in\n\ +\ --win-per-user-install\n\ +\ Request to perform an install on a per-user basis\n\ +\ --win-shortcut\n\ +\ Creates a desktop shortcut for the application\n\ +\ --win-upgrade-uuid \n\ +\ UUID associated with upgrades for this package\n\ + +MSG_Help_win_install_dir=\ +\Relative sub-path under the default installation location\n\ + +MSG_Help_mac_launcher=\ +\ --mac-package-identifier \n\ +\ An identifier that uniquely identifies the application for macOS\n\ +\ Defaults to the main class name.\n\ +\ May only use alphanumeric (A-Z,a-z,0-9), hyphen (-),\n\ +\ and period (.) characters.\n\ +\ --mac-package-name \n\ +\ Name of the application as it appears in the Menu Bar\n\ +\ This can be different from the application name.\n\ +\ This name must be less than 16 characters long and be suitable for\n\ +\ displaying in the menu bar and the application Info window.\n\ +\ Defaults to the application name.\n\ +\ --mac-package-signing-prefix \n\ +\ When signing the application package, this value is prefixed\n\ +\ to all components that need to be signed that don't have\n\ +\ an existing package identifier.\n\ +\ --mac-sign\n\ +\ Request that the package be signed\n\ +\ --mac-signing-keychain \n\ +\ Path of the keychain to search for the signing identity\n\ +\ (absolute path or relative to the current directory).\n\ +\ If not specified, the standard keychains are used.\n\ +\ --mac-signing-key-user-name \n\ +\ Team name portion in Apple signing identities' names.\n\ +\ For example "Developer ID Application: "\n\ + +MSG_Help_linux_install=\ +\ --linux-package-name \n\ +\ Name for Linux package, defaults to the application name\n\ +\ --linux-deb-maintainer \n\ +\ Maintainer for .deb package\n\ +\ --linux-menu-group \n\ +\ Menu group this application is placed in\n\ +\ --linux-package-deps\n\ +\ Required packages or capabilities for the application\n\ +\ --linux-rpm-license-type \n\ +\ Type of the license ("License: " of the RPM .spec)\n\ +\ --linux-app-release \n\ +\ Release value of the RPM .spec file or \n\ +\ Debian revision value of the DEB control file.\n\ +\ --linux-app-category \n\ +\ Group value of the RPM .spec file or \n\ +\ Section value of DEB control file.\n\ +\ --linux-shortcut\n\ +\ Creates a shortcut for the application\n\ + +MSG_Help_mac_linux_install_dir=\ +\Absolute path of the installation directory of the application\n\ + +MSG_Help_default_install_dir=\ +\Absolute path of the installation directory of the application on OS X\n\ +\ or Linux. Relative sub-path of the installation location of\n\ +\ the application such as "Program Files" or "AppData" on Windows.\n\ + +MSG_Help_no_args=Usage: jpackage \n\ +\Use jpackage --help (or -h) for a list of possible options\ + diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/HelpResources_ja.properties b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/HelpResources_ja.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/HelpResources_ja.properties @@ -0,0 +1,275 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +MSG_Help=Usage: jpackage \n\ +\n\ +Sample usages:\n\ +--------------\n\ +\ Generate an application package suitable for the host system:\n\ +\ For a modular application:\n\ +\ jpackage -n name -p modulePath -m moduleName/className\n\ +\ For a non-modular application:\n\ +\ jpackage -i inputDir -n name \\\n\ +\ --main-class className --main-jar myJar.jar\n\ +\ From a pre-built application image:\n\ +\ jpackage -n name --app-image appImageDir\n\ +\ Generate an application image:\n\ +\ For a modular application:\n\ +\ jpackage --type app-image -n name -p modulePath \\\n\ +\ -m moduleName/className\n\ +\ For a non-modular application:\n\ +\ jpackage --type app-image -i inputDir -n name \\\n\ +\ --main-class className --main-jar myJar.jar\n\ +\ To provide your own options to jlink, run jlink separately:\n\ +\ jlink --output appRuntimeImage -p modulePath -m moduleName \\\n\ +\ --no-header-files [...]\n\ +\ jpackage --type app-image -n name \\\n\ +\ -m moduleName/className --runtime-image appRuntimeImage\n\ +\ Generate a Java runtime package:\n\ +\ jpackage -n name --runtime-image \n\ +\n\ +Generic Options:\n\ +\ @ \n\ +\ Read options and/or mode from a file \n\ +\ This option can be used multiple times.\n\ +\ --type -t \n\ +\ The type of package to create\n\ +\ Valid values are: {1} \n\ +\ If this option is not specified a platform dependent\n\ +\ default type will be created.\n\ +\ --app-version \n\ +\ Version of the application and/or package\n\ +\ --copyright \n\ +\ Copyright for the application\n\ +\ --description \n\ +\ Description of the application\n\ +\ --help -h \n\ +\ Print the usage text with a list and description of each valid\n\ +\ option for the current platform to the output stream, and exit\n\ +\ --name -n \n\ +\ Name of the application and/or package\n\ +\ --dest -d \n\ +\ Path where generated output file is placed\n\ +\ Defaults to the current working directory.\n\ +\ (absolute path or relative to the current directory)\n\ +\ --temp \n\ +\ Path of a new or empty directory used to create temporary files\n\ +\ (absolute path or relative to the current directory)\n\ +\ If specified, the temp dir will not be removed upon the task\n\ +\ completion and must be removed manually\n\ +\ If not specified, a temporary directory will be created and\n\ +\ removed upon the task completion.\n\ +\ --vendor \n\ +\ Vendor of the application\n\ +\ --verbose\n\ +\ Enables verbose output\n\ +\ --version\n\ +\ Print the product version to the output stream and exit\n\ +\n\ +\Options for creating the runtime image:\n\ +\ --add-modules [,...]\n\ +\ A comma (",") separated list of modules to add.\n\ +\ This module list, along with the main module (if specified)\n\ +\ will be passed to jlink as the --add-module argument.\n\ +\ if not specified, either just the main module (if --module is\n\ +\ specified), or the default set of modules (if --main-jar is \n\ +\ specified) are used.\n\ +\ This option can be used multiple times.\n\ +\ --module-path -p ...\n\ +\ A {0} separated list of paths\n\ +\ Each path is either a directory of modules or the path to a\n\ +\ modular jar.\n\ +\ (each path is absolute or relative to the current directory)\n\ +\ This option can be used multiple times.\n\ +\ --bind-services \n\ +\ Pass on --bind-services option to jlink (which will link in \n\ +\ service provider modules and their dependences) \n\ +\ --runtime-image \n\ +\ Path of the predefined runtime image that will be copied into\n\ +\ the application image\n\ +\ (absolute path or relative to the current directory)\n\ +\ If --runtime-image is not specified, jpackage will run jlink to\n\ +\ create the runtime image using options:\n\ +\ --strip-debug, --no-header-files, --no-man-pages, and\n\ +\ --strip-native-commands.\n\ +\n\ +\Options for creating the application image:\n\ +\ --icon \n\ +\ Path of the icon of the application package\n\ +\ (absolute path or relative to the current directory)\n\ +\ --input -i \n\ +\ Path of the input directory that contains the files to be packaged\n\ +\ (absolute path or relative to the current directory)\n\ +\ All files in the input directory will be packaged into the\n\ +\ application image.\n\ +\n\ +\Options for creating the application launcher(s):\n\ +\ --add-launcher =\n\ +\ Name of launcher, and a path to a Properties file that contains\n\ +\ a list of key, value pairs\n\ +\ (absolute path or relative to the current directory)\n\ +\ The keys "module", "main-jar", "main-class",\n\ +\ "arguments", "java-options", "app-version", "icon", and\n\ +\ "win-console" can be used.\n\ +\ These options are added to, or used to overwrite, the original\n\ +\ command line options to build an additional alternative launcher.\n\ +\ The main application launcher will be built from the command line\n\ +\ options. Additional alternative launchers can be built using\n\ +\ this option, and this option can be used multiple times to\n\ +\ build multiple additional launchers. \n\ +\ --arguments
\n\ +\ Command line arguments to pass to the main class if no command\n\ +\ line arguments are given to the launcher\n\ +\ This option can be used multiple times.\n\ +\ --java-options \n\ +\ Options to pass to the Java runtime\n\ +\ This option can be used multiple times.\n\ +\ --main-class \n\ +\ Qualified name of the application main class to execute\n\ +\ This option can only be used if --main-jar is specified.\n\ +\ --main-jar
\n\ +\ The main JAR of the application; containing the main class\n\ +\ (specified as a path relative to the input path)\n\ +\ Either --module or --main-jar option can be specified but not\n\ +\ both.\n\ +\ --module -m [/
]\n\ +\ The main module (and optionally main class) of the application\n\ +\ This module must be located on the module path.\n\ +\ When this option is specified, the main module will be linked\n\ +\ in the Java runtime image. Either --module or --main-jar\n\ +\ option can be specified but not both.\n\ +{2}\n\ +\Options for creating the application package:\n\ +\ --app-image \n\ +\ Location of the predefined application image that is used\n\ +\ to build an installable package\n\ +\ (absolute path or relative to the current directory)\n\ +\ --file-associations \n\ +\ Path to a Properties file that contains list of key, value pairs\n\ +\ (absolute path or relative to the current directory)\n\ +\ The keys "extension", "mime-type", "icon", and "description"\n\ +\ can be used to describe the association.\n\ +\ This option can be used multiple times.\n\ +\ --install-dir \n\ +\ {4}\ +\ --license-file \n\ +\ Path to the license file\n\ +\ (absolute path or relative to the current directory)\n\ +\ --resource-dir \n\ +\ Path to override jpackage resources\n\ +\ Icons, template files, and other resources of jpackage can be\n\ +\ over-ridden by adding replacement resources to this directory.\n\ +\ (absolute path or relative to the current directory)\n\ +\ --runtime-image \n\ +\ Path of the predefined runtime image to install\n\ +\ (absolute path or relative to the current directory)\n\ +\ Option is required when creating a runtime package.\n\ +\n\ +\Platform dependent options for creating the application package:\n\ +{3} + +MSG_Help_win_launcher=\ +\n\ +\Platform dependent option for creating the application launcher:\n\ +\ --win-console\n\ +\ Creates a console launcher for the application, should be\n\ +\ specified for application which requires console interactions\n\ + +MSG_Help_win_install=\ +\ --win-dir-chooser\n\ +\ Adds a dialog to enable the user to choose a directory in which\n\ +\ the product is installed\n\ +\ --win-menu\n\ +\ Adds the application to the system menu\n\ +\ --win-menu-group \n\ +\ Start Menu group this application is placed in\n\ +\ --win-per-user-install\n\ +\ Request to perform an install on a per-user basis\n\ +\ --win-shortcut\n\ +\ Creates a desktop shortcut for the application\n\ +\ --win-upgrade-uuid \n\ +\ UUID associated with upgrades for this package\n\ + +MSG_Help_win_install_dir=\ +\Relative sub-path under the default installation location\n\ + +MSG_Help_mac_launcher=\ +\ --mac-package-identifier \n\ +\ An identifier that uniquely identifies the application for macOS\n\ +\ Defaults to the main class name.\n\ +\ May only use alphanumeric (A-Z,a-z,0-9), hyphen (-),\n\ +\ and period (.) characters.\n\ +\ --mac-package-name \n\ +\ Name of the application as it appears in the Menu Bar\n\ +\ This can be different from the application name.\n\ +\ This name must be less than 16 characters long and be suitable for\n\ +\ displaying in the menu bar and the application Info window.\n\ +\ Defaults to the application name.\n\ +\ --mac-package-signing-prefix \n\ +\ When signing the application package, this value is prefixed\n\ +\ to all components that need to be signed that don't have\n\ +\ an existing package identifier.\n\ +\ --mac-sign\n\ +\ Request that the package be signed\n\ +\ --mac-signing-keychain \n\ +\ Path of the keychain to search for the signing identity\n\ +\ (absolute path or relative to the current directory).\n\ +\ If not specified, the standard keychains are used.\n\ +\ --mac-signing-key-user-name \n\ +\ Team name portion in Apple signing identities' names.\n\ +\ For example "Developer ID Application: "\n\ + +MSG_Help_linux_install=\ +\ --linux-package-name \n\ +\ Name for Linux package, defaults to the application name\n\ +\ --linux-deb-maintainer \n\ +\ Maintainer for .deb package\n\ +\ --linux-menu-group \n\ +\ Menu group this application is placed in\n\ +\ --linux-package-deps\n\ +\ Required packages or capabilities for the application\n\ +\ --linux-rpm-license-type \n\ +\ Type of the license ("License: " of the RPM .spec)\n\ +\ --linux-app-release \n\ +\ Release value of the RPM .spec file or \n\ +\ Debian revision value of the DEB control file.\n\ +\ --linux-app-category \n\ +\ Group value of the RPM .spec file or \n\ +\ Section value of DEB control file.\n\ +\ --linux-shortcut\n\ +\ Creates a shortcut for the application\n\ + +MSG_Help_mac_linux_install_dir=\ +\Absolute path of the installation directory of the application\n\ + +MSG_Help_default_install_dir=\ +\Absolute path of the installation directory of the application on OS X\n\ +\ or Linux. Relative sub-path of the installation location of\n\ +\ the application such as "Program Files" or "AppData" on Windows.\n\ + +MSG_Help_no_args=Usage: jpackage \n\ +\Use jpackage --help (or -h) for a list of possible options\ + diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/HelpResources_zh_CN.properties b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/HelpResources_zh_CN.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/HelpResources_zh_CN.properties @@ -0,0 +1,275 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +MSG_Help=Usage: jpackage \n\ +\n\ +Sample usages:\n\ +--------------\n\ +\ Generate an application package suitable for the host system:\n\ +\ For a modular application:\n\ +\ jpackage -n name -p modulePath -m moduleName/className\n\ +\ For a non-modular application:\n\ +\ jpackage -i inputDir -n name \\\n\ +\ --main-class className --main-jar myJar.jar\n\ +\ From a pre-built application image:\n\ +\ jpackage -n name --app-image appImageDir\n\ +\ Generate an application image:\n\ +\ For a modular application:\n\ +\ jpackage --type app-image -n name -p modulePath \\\n\ +\ -m moduleName/className\n\ +\ For a non-modular application:\n\ +\ jpackage --type app-image -i inputDir -n name \\\n\ +\ --main-class className --main-jar myJar.jar\n\ +\ To provide your own options to jlink, run jlink separately:\n\ +\ jlink --output appRuntimeImage -p modulePath -m moduleName \\\n\ +\ --no-header-files [...]\n\ +\ jpackage --type app-image -n name \\\n\ +\ -m moduleName/className --runtime-image appRuntimeImage\n\ +\ Generate a Java runtime package:\n\ +\ jpackage -n name --runtime-image \n\ +\n\ +Generic Options:\n\ +\ @ \n\ +\ Read options and/or mode from a file \n\ +\ This option can be used multiple times.\n\ +\ --type -t \n\ +\ The type of package to create\n\ +\ Valid values are: {1} \n\ +\ If this option is not specified a platform dependent\n\ +\ default type will be created.\n\ +\ --app-version \n\ +\ Version of the application and/or package\n\ +\ --copyright \n\ +\ Copyright for the application\n\ +\ --description \n\ +\ Description of the application\n\ +\ --help -h \n\ +\ Print the usage text with a list and description of each valid\n\ +\ option for the current platform to the output stream, and exit\n\ +\ --name -n \n\ +\ Name of the application and/or package\n\ +\ --dest -d \n\ +\ Path where generated output file is placed\n\ +\ Defaults to the current working directory.\n\ +\ (absolute path or relative to the current directory)\n\ +\ --temp \n\ +\ Path of a new or empty directory used to create temporary files\n\ +\ (absolute path or relative to the current directory)\n\ +\ If specified, the temp dir will not be removed upon the task\n\ +\ completion and must be removed manually\n\ +\ If not specified, a temporary directory will be created and\n\ +\ removed upon the task completion.\n\ +\ --vendor \n\ +\ Vendor of the application\n\ +\ --verbose\n\ +\ Enables verbose output\n\ +\ --version\n\ +\ Print the product version to the output stream and exit\n\ +\n\ +\Options for creating the runtime image:\n\ +\ --add-modules [,...]\n\ +\ A comma (",") separated list of modules to add.\n\ +\ This module list, along with the main module (if specified)\n\ +\ will be passed to jlink as the --add-module argument.\n\ +\ if not specified, either just the main module (if --module is\n\ +\ specified), or the default set of modules (if --main-jar is \n\ +\ specified) are used.\n\ +\ This option can be used multiple times.\n\ +\ --module-path -p ...\n\ +\ A {0} separated list of paths\n\ +\ Each path is either a directory of modules or the path to a\n\ +\ modular jar.\n\ +\ (each path is absolute or relative to the current directory)\n\ +\ This option can be used multiple times.\n\ +\ --bind-services \n\ +\ Pass on --bind-services option to jlink (which will link in \n\ +\ service provider modules and their dependences) \n\ +\ --runtime-image \n\ +\ Path of the predefined runtime image that will be copied into\n\ +\ the application image\n\ +\ (absolute path or relative to the current directory)\n\ +\ If --runtime-image is not specified, jpackage will run jlink to\n\ +\ create the runtime image using options:\n\ +\ --strip-debug, --no-header-files, --no-man-pages, and\n\ +\ --strip-native-commands.\n\ +\n\ +\Options for creating the application image:\n\ +\ --icon \n\ +\ Path of the icon of the application package\n\ +\ (absolute path or relative to the current directory)\n\ +\ --input -i \n\ +\ Path of the input directory that contains the files to be packaged\n\ +\ (absolute path or relative to the current directory)\n\ +\ All files in the input directory will be packaged into the\n\ +\ application image.\n\ +\n\ +\Options for creating the application launcher(s):\n\ +\ --add-launcher =\n\ +\ Name of launcher, and a path to a Properties file that contains\n\ +\ a list of key, value pairs\n\ +\ (absolute path or relative to the current directory)\n\ +\ The keys "module", "main-jar", "main-class",\n\ +\ "arguments", "java-options", "app-version", "icon", and\n\ +\ "win-console" can be used.\n\ +\ These options are added to, or used to overwrite, the original\n\ +\ command line options to build an additional alternative launcher.\n\ +\ The main application launcher will be built from the command line\n\ +\ options. Additional alternative launchers can be built using\n\ +\ this option, and this option can be used multiple times to\n\ +\ build multiple additional launchers. \n\ +\ --arguments
\n\ +\ Command line arguments to pass to the main class if no command\n\ +\ line arguments are given to the launcher\n\ +\ This option can be used multiple times.\n\ +\ --java-options \n\ +\ Options to pass to the Java runtime\n\ +\ This option can be used multiple times.\n\ +\ --main-class \n\ +\ Qualified name of the application main class to execute\n\ +\ This option can only be used if --main-jar is specified.\n\ +\ --main-jar
\n\ +\ The main JAR of the application; containing the main class\n\ +\ (specified as a path relative to the input path)\n\ +\ Either --module or --main-jar option can be specified but not\n\ +\ both.\n\ +\ --module -m [/
]\n\ +\ The main module (and optionally main class) of the application\n\ +\ This module must be located on the module path.\n\ +\ When this option is specified, the main module will be linked\n\ +\ in the Java runtime image. Either --module or --main-jar\n\ +\ option can be specified but not both.\n\ +{2}\n\ +\Options for creating the application package:\n\ +\ --app-image \n\ +\ Location of the predefined application image that is used\n\ +\ to build an installable package\n\ +\ (absolute path or relative to the current directory)\n\ +\ --file-associations \n\ +\ Path to a Properties file that contains list of key, value pairs\n\ +\ (absolute path or relative to the current directory)\n\ +\ The keys "extension", "mime-type", "icon", and "description"\n\ +\ can be used to describe the association.\n\ +\ This option can be used multiple times.\n\ +\ --install-dir \n\ +\ {4}\ +\ --license-file \n\ +\ Path to the license file\n\ +\ (absolute path or relative to the current directory)\n\ +\ --resource-dir \n\ +\ Path to override jpackage resources\n\ +\ Icons, template files, and other resources of jpackage can be\n\ +\ over-ridden by adding replacement resources to this directory.\n\ +\ (absolute path or relative to the current directory)\n\ +\ --runtime-image \n\ +\ Path of the predefined runtime image to install\n\ +\ (absolute path or relative to the current directory)\n\ +\ Option is required when creating a runtime package.\n\ +\n\ +\Platform dependent options for creating the application package:\n\ +{3} + +MSG_Help_win_launcher=\ +\n\ +\Platform dependent option for creating the application launcher:\n\ +\ --win-console\n\ +\ Creates a console launcher for the application, should be\n\ +\ specified for application which requires console interactions\n\ + +MSG_Help_win_install=\ +\ --win-dir-chooser\n\ +\ Adds a dialog to enable the user to choose a directory in which\n\ +\ the product is installed\n\ +\ --win-menu\n\ +\ Adds the application to the system menu\n\ +\ --win-menu-group \n\ +\ Start Menu group this application is placed in\n\ +\ --win-per-user-install\n\ +\ Request to perform an install on a per-user basis\n\ +\ --win-shortcut\n\ +\ Creates a desktop shortcut for the application\n\ +\ --win-upgrade-uuid \n\ +\ UUID associated with upgrades for this package\n\ + +MSG_Help_win_install_dir=\ +\Relative sub-path under the default installation location\n\ + +MSG_Help_mac_launcher=\ +\ --mac-package-identifier \n\ +\ An identifier that uniquely identifies the application for macOS\n\ +\ Defaults to the main class name.\n\ +\ May only use alphanumeric (A-Z,a-z,0-9), hyphen (-),\n\ +\ and period (.) characters.\n\ +\ --mac-package-name \n\ +\ Name of the application as it appears in the Menu Bar\n\ +\ This can be different from the application name.\n\ +\ This name must be less than 16 characters long and be suitable for\n\ +\ displaying in the menu bar and the application Info window.\n\ +\ Defaults to the application name.\n\ +\ --mac-package-signing-prefix \n\ +\ When signing the application package, this value is prefixed\n\ +\ to all components that need to be signed that don't have\n\ +\ an existing package identifier.\n\ +\ --mac-sign\n\ +\ Request that the package be signed\n\ +\ --mac-signing-keychain \n\ +\ Path of the keychain to search for the signing identity\n\ +\ (absolute path or relative to the current directory).\n\ +\ If not specified, the standard keychains are used.\n\ +\ --mac-signing-key-user-name \n\ +\ Team name portion in Apple signing identities' names.\n\ +\ For example "Developer ID Application: "\n\ + +MSG_Help_linux_install=\ +\ --linux-package-name \n\ +\ Name for Linux package, defaults to the application name\n\ +\ --linux-deb-maintainer \n\ +\ Maintainer for .deb package\n\ +\ --linux-menu-group \n\ +\ Menu group this application is placed in\n\ +\ --linux-package-deps\n\ +\ Required packages or capabilities for the application\n\ +\ --linux-rpm-license-type \n\ +\ Type of the license ("License: " of the RPM .spec)\n\ +\ --linux-app-release \n\ +\ Release value of the RPM .spec file or \n\ +\ Debian revision value of the DEB control file.\n\ +\ --linux-app-category \n\ +\ Group value of the RPM .spec file or \n\ +\ Section value of DEB control file.\n\ +\ --linux-shortcut\n\ +\ Creates a shortcut for the application\n\ + +MSG_Help_mac_linux_install_dir=\ +\Absolute path of the installation directory of the application\n\ + +MSG_Help_default_install_dir=\ +\Absolute path of the installation directory of the application on OS X\n\ +\ or Linux. Relative sub-path of the installation location of\n\ +\ the application such as "Program Files" or "AppData" on Windows.\n\ + +MSG_Help_no_args=Usage: jpackage \n\ +\Use jpackage --help (or -h) for a list of possible options\ + diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/MainResources.properties b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/MainResources.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/MainResources.properties @@ -0,0 +1,92 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +param.copyright.default=Copyright (C) {0,date,YYYY} +param.description.default=None +param.vendor.default=Unknown + +message.using-default-resource=Using default package resource {0} {1} (add {2} to the resource-dir to customize). +message.no-default-resource=no default package resource {0} {1} (add {2} to the resource-dir to customize). +message.using-custom-resource-from-file=Using custom package resource {0} (loaded from file {1}). +message.using-custom-resource=Using custom package resource {0} (loaded from {1}). +message.creating-app-bundle=Creating app package: {0} in {1} +message.app-image-dir-does-not-exist=Specified application image directory {0}: {1} does not exists +message.app-image-dir-does-not-exist.advice=Confirm that the value for {0} exists +message.runtime-image-dir-does-not-exist=Specified runtime image directory {0}: {1} does not exists +message.runtime-image-dir-does-not-exist.advice=Confirm that the value for {0} exists +message.debug-working-directory=Kept working directory for debug: {0} +message.bundle-created=Succeeded in building {0} package +message.module-version=Using version "{0}" from module "{1}" as application version +message.module-class=Using class "{0}" from module "{1}" as application main class + +error.cannot-create-output-dir=Destination directory {0} cannot be created +error.cannot-write-to-output-dir=Destination directory {0} is not writable +error.root-exists=Error: Application destination directory {0} already exists +error.no-main-class-with-main-jar=A main class was not specified nor was one found in the jar {0} +error.no-main-class-with-main-jar.advice=Specify a main class or ensure that the jar {0} specifies one in the manifest +error.no-main-class=A main class was not specified nor was one found in the supplied application resources +error.no-main-class.advice=Please specify a application class or ensure that the appResources has a jar containing one in the manifest +error.main-jar-does-not-exist=The configured main jar does not exist {0} in the input directory +error.main-jar-does-not-exist.advice=The main jar must be specified relative to the input directory (not an absolute path), and must exist within that directory + +error.tool-not-found=Can not find {0}. Reason: {1} +error.tool-not-found.advice=Please install {0} +error.tool-old-version=Can not find {0} {1} or newer +error.tool-old-version.advice=Please install {0} {1} or newer +error.jlink.failed=jlink failed with: {0} + +warning.module.does.not.exist=Module [{0}] does not exist +warning.no.jdk.modules.found=Warning: No JDK Modules found + +MSG_BundlerFailed=Error: Bundler "{1}" ({0}) failed to produce a package +MSG_BundlerConfigException=Bundler {0} skipped because of a configuration problem: {1} \n\ +Advice to fix: {2} +MSG_BundlerConfigExceptionNoAdvice=Bundler {0} skipped because of a configuration problem: {1} +MSG_BundlerRuntimeException=Bundler {0} failed because of {1} +MSG_BundlerFailed=Error: Bundler "{1}" ({0}) failed to produce a package + +ERR_NoMainClass=Error: Main application class is missing +ERR_UnsupportedOption=Error: Option [{0}] is not valid on this platform +ERR_InvalidTypeOption=Error: Option [{0}] is not valid with type [{1}] +ERR_NoInstallerEntryPoint=Error: Option [{0}] is not valid without --module or --main-jar entry point option + +ERR_MissingArgument=Error: Missing argument: {0} +ERR_MissingAppResources=Error: No application jars found +ERR_AppImageNotExist=Error: App image directory "{0}" does not exist +ERR_NoAddLauncherName=Error: --add-launcher option requires a name and a file path (--add-launcher =) +ERR_NoUniqueName=Error: --add-launcher = requires a unique name +ERR_NoJreInstallerName=Error: Jre Installers require a name parameter +ERR_InvalidAppName=Error: Invalid Application name: {0} +ERR_InvalidSLName=Error: Invalid Add Launcher name: {0} +ERR_LicenseFileNotExit=Error: Specified license file does not exist +ERR_BuildRootInvalid=Error: temp ({0}) must be non-existant or empty directory +ERR_InvalidOption=Error: Invalid Option: [{0}] +ERR_InvalidInstallerType=Error: Invalid or unsupported type: [{0}] +ERR_BothMainJarAndModule=Error: Cannot have both --main-jar and --module Options +ERR_NoEntryPoint=Error: creating application image requires --main-jar or --module Option +ERR_InputNotDirectory=Error: Input directory specified is not a directory: {0} +ERR_CannotReadInputDir=Error: No permission to read from input directory: {0} +ERR_CannotParseOptions=Error: Processing @filename option: {0} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/MainResources_ja.properties b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/MainResources_ja.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/MainResources_ja.properties @@ -0,0 +1,92 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +param.copyright.default=Copyright (C) {0,date,YYYY} +param.description.default=None +param.vendor.default=Unknown + +message.using-default-resource=Using default package resource {0} {1} (add {2} to the resource-dir to customize). +message.no-default-resource=no default package resource {0} {1} (add {2} to the resource-dir to customize). +message.using-custom-resource-from-file=Using custom package resource {0} (loaded from file {1}). +message.using-custom-resource=Using custom package resource {0} (loaded from {1}). +message.creating-app-bundle=Creating app package: {0} in {1} +message.app-image-dir-does-not-exist=Specified application image directory {0}: {1} does not exists +message.app-image-dir-does-not-exist.advice=Confirm that the value for {0} exists +message.runtime-image-dir-does-not-exist=Specified runtime image directory {0}: {1} does not exists +message.runtime-image-dir-does-not-exist.advice=Confirm that the value for {0} exists +message.debug-working-directory=Kept working directory for debug: {0} +message.bundle-created=Succeeded in building {0} package +message.module-version=Using version "{0}" from module "{1}" as application version +message.module-class=Using class "{0}" from module "{1}" as application main class + +error.cannot-create-output-dir=Destination directory {0} cannot be created +error.cannot-write-to-output-dir=Destination directory {0} is not writable +error.root-exists=Error: Application destination directory {0} already exists +error.no-main-class-with-main-jar=A main class was not specified nor was one found in the jar {0} +error.no-main-class-with-main-jar.advice=Specify a main class or ensure that the jar {0} specifies one in the manifest +error.no-main-class=A main class was not specified nor was one found in the supplied application resources +error.no-main-class.advice=Please specify a application class or ensure that the appResources has a jar containing one in the manifest +error.main-jar-does-not-exist=The configured main jar does not exist {0} in the input directory +error.main-jar-does-not-exist.advice=The main jar must be specified relative to the input directory (not an absolute path), and must exist within that directory + +error.tool-not-found=Can not find {0}. Reason: {1} +error.tool-not-found.advice=Please install {0} +error.tool-old-version=Can not find {0} {1} or newer +error.tool-old-version.advice=Please install {0} {1} or newer +error.jlink.failed=jlink failed with: {0} + +warning.module.does.not.exist=Module [{0}] does not exist +warning.no.jdk.modules.found=Warning: No JDK Modules found + +MSG_BundlerFailed=Error: Bundler "{1}" ({0}) failed to produce a package +MSG_BundlerConfigException=Bundler {0} skipped because of a configuration problem: {1} \n\ +Advice to fix: {2} +MSG_BundlerConfigExceptionNoAdvice=Bundler {0} skipped because of a configuration problem: {1} +MSG_BundlerRuntimeException=Bundler {0} failed because of {1} +MSG_BundlerFailed=Error: Bundler "{1}" ({0}) failed to produce a package + +ERR_NoMainClass=Error: Main application class is missing +ERR_UnsupportedOption=Error: Option [{0}] is not valid on this platform +ERR_InvalidTypeOption=Error: Option [{0}] is not valid with type [{1}] +ERR_NoInstallerEntryPoint=Error: Option [{0}] is not valid without --module or --main-jar entry point option + +ERR_MissingArgument=Error: Missing argument: {0} +ERR_MissingAppResources=Error: No application jars found +ERR_AppImageNotExist=Error: App image directory "{0}" does not exist +ERR_NoAddLauncherName=Error: --add-launcher option requires a name and a file path (--add-launcher =) +ERR_NoUniqueName=Error: --add-launcher = requires a unique name +ERR_NoJreInstallerName=Error: Jre Installers require a name parameter +ERR_InvalidAppName=Error: Invalid Application name: {0} +ERR_InvalidSLName=Error: Invalid Add Launcher name: {0} +ERR_LicenseFileNotExit=Error: Specified license file does not exist +ERR_BuildRootInvalid=Error: temp ({0}) must be non-existant or empty directory +ERR_InvalidOption=Error: Invalid Option: [{0}] +ERR_InvalidInstallerType=Error: Invalid or unsupported type: [{0}] +ERR_BothMainJarAndModule=Error: Cannot have both --main-jar and --module Options +ERR_NoEntryPoint=Error: creating application image requires --main-jar or --module Option +ERR_InputNotDirectory=Error: Input directory specified is not a directory: {0} +ERR_CannotReadInputDir=Error: No permission to read from input directory: {0} +ERR_CannotParseOptions=Error: Processing @filename option: {0} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/MainResources_zh_CN.properties b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/MainResources_zh_CN.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/MainResources_zh_CN.properties @@ -0,0 +1,92 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +param.copyright.default=Copyright (C) {0,date,YYYY} +param.description.default=None +param.vendor.default=Unknown + +message.using-default-resource=Using default package resource {0} {1} (add {2} to the resource-dir to customize). +message.no-default-resource=no default package resource {0} {1} (add {2} to the resource-dir to customize). +message.using-custom-resource-from-file=Using custom package resource {0} (loaded from file {1}). +message.using-custom-resource=Using custom package resource {0} (loaded from {1}). +message.creating-app-bundle=Creating app package: {0} in {1} +message.app-image-dir-does-not-exist=Specified application image directory {0}: {1} does not exists +message.app-image-dir-does-not-exist.advice=Confirm that the value for {0} exists +message.runtime-image-dir-does-not-exist=Specified runtime image directory {0}: {1} does not exists +message.runtime-image-dir-does-not-exist.advice=Confirm that the value for {0} exists +message.debug-working-directory=Kept working directory for debug: {0} +message.bundle-created=Succeeded in building {0} package +message.module-version=Using version "{0}" from module "{1}" as application version +message.module-class=Using class "{0}" from module "{1}" as application main class + +error.cannot-create-output-dir=Destination directory {0} cannot be created +error.cannot-write-to-output-dir=Destination directory {0} is not writable +error.root-exists=Error: Application destination directory {0} already exists +error.no-main-class-with-main-jar=A main class was not specified nor was one found in the jar {0} +error.no-main-class-with-main-jar.advice=Specify a main class or ensure that the jar {0} specifies one in the manifest +error.no-main-class=A main class was not specified nor was one found in the supplied application resources +error.no-main-class.advice=Please specify a application class or ensure that the appResources has a jar containing one in the manifest +error.main-jar-does-not-exist=The configured main jar does not exist {0} in the input directory +error.main-jar-does-not-exist.advice=The main jar must be specified relative to the input directory (not an absolute path), and must exist within that directory + +error.tool-not-found=Can not find {0}. Reason: {1} +error.tool-not-found.advice=Please install {0} +error.tool-old-version=Can not find {0} {1} or newer +error.tool-old-version.advice=Please install {0} {1} or newer +error.jlink.failed=jlink failed with: {0} + +warning.module.does.not.exist=Module [{0}] does not exist +warning.no.jdk.modules.found=Warning: No JDK Modules found + +MSG_BundlerFailed=Error: Bundler "{1}" ({0}) failed to produce a package +MSG_BundlerConfigException=Bundler {0} skipped because of a configuration problem: {1} \n\ +Advice to fix: {2} +MSG_BundlerConfigExceptionNoAdvice=Bundler {0} skipped because of a configuration problem: {1} +MSG_BundlerRuntimeException=Bundler {0} failed because of {1} +MSG_BundlerFailed=Error: Bundler "{1}" ({0}) failed to produce a package + +ERR_NoMainClass=Error: Main application class is missing +ERR_UnsupportedOption=Error: Option [{0}] is not valid on this platform +ERR_InvalidTypeOption=Error: Option [{0}] is not valid with type [{1}] +ERR_NoInstallerEntryPoint=Error: Option [{0}] is not valid without --module or --main-jar entry point option + +ERR_MissingArgument=Error: Missing argument: {0} +ERR_MissingAppResources=Error: No application jars found +ERR_AppImageNotExist=Error: App image directory "{0}" does not exist +ERR_NoAddLauncherName=Error: --add-launcher option requires a name and a file path (--add-launcher =) +ERR_NoUniqueName=Error: --add-launcher = requires a unique name +ERR_NoJreInstallerName=Error: Jre Installers require a name parameter +ERR_InvalidAppName=Error: Invalid Application name: {0} +ERR_InvalidSLName=Error: Invalid Add Launcher name: {0} +ERR_LicenseFileNotExit=Error: Specified license file does not exist +ERR_BuildRootInvalid=Error: temp ({0}) must be non-existant or empty directory +ERR_InvalidOption=Error: Invalid Option: [{0}] +ERR_InvalidInstallerType=Error: Invalid or unsupported type: [{0}] +ERR_BothMainJarAndModule=Error: Cannot have both --main-jar and --module Options +ERR_NoEntryPoint=Error: creating application image requires --main-jar or --module Option +ERR_InputNotDirectory=Error: Input directory specified is not a directory: {0} +ERR_CannotReadInputDir=Error: No permission to read from input directory: {0} +ERR_CannotParseOptions=Error: Processing @filename option: {0} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/ResourceLocator.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/ResourceLocator.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/ResourceLocator.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2011, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal.resources; + +/* + * ResourceLocator + * This empty class is the only class in this package. Otherwise the + * package consists only of resources. ResourceLocator is needed in order + * to call getResourceAsStream() to get those resources. + */ + +public class ResourceLocator { + public ResourceLocator() { + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/main/CommandLine.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/main/CommandLine.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/main/CommandLine.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 1999, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.main; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.File; +import java.io.Reader; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * This file was originally a copy of CommandLine.java in + * com.sun.tools.javac.main. + * It should track changes made to that file. + */ + +/** + * Various utility methods for processing Java tool command line arguments. + * + *

This is NOT part of any supported API. + * If you write code that depends on this, you do so at your own risk. + * This code and its internal interfaces are subject to change or + * deletion without notice. + */ +class CommandLine { + /** + * Process Win32-style command files for the specified command line + * arguments and return the resulting arguments. A command file argument + * is of the form '@file' where 'file' is the name of the file whose + * contents are to be parsed for additional arguments. The contents of + * the command file are parsed using StreamTokenizer and the original + * '@file' argument replaced with the resulting tokens. Recursive command + * files are not supported. The '@' character itself can be quoted with + * the sequence '@@'. + * @param args the arguments that may contain @files + * @return the arguments, with @files expanded + * @throws IOException if there is a problem reading any of the @files + */ + public static String[] parse(String[] args) throws IOException { + List newArgs = new ArrayList<>(); + appendParsedCommandArgs(newArgs, Arrays.asList(args)); + return newArgs.toArray(new String[newArgs.size()]); + } + + private static void appendParsedCommandArgs(List newArgs, + List args) throws IOException { + for (String arg : args) { + if (arg.length() > 1 && arg.charAt(0) == '@') { + arg = arg.substring(1); + if (arg.charAt(0) == '@') { + newArgs.add(arg); + } else { + loadCmdFile(arg, newArgs); + } + } else { + newArgs.add(arg); + } + } + } + + private static void loadCmdFile(String name, List args) + throws IOException { + if (!Files.isReadable(Path.of(name))) { + throw new FileNotFoundException(name); + } + try (Reader r = Files.newBufferedReader(Paths.get(name), + Charset.defaultCharset())) { + Tokenizer t = new Tokenizer(r); + String s; + while ((s = t.nextToken()) != null) { + args.add(s); + } + } + } + + public static class Tokenizer { + private final Reader in; + private int ch; + + public Tokenizer(Reader in) throws IOException { + this.in = in; + ch = in.read(); + } + + public String nextToken() throws IOException { + skipWhite(); + if (ch == -1) { + return null; + } + + StringBuilder sb = new StringBuilder(); + char quoteChar = 0; + + while (ch != -1) { + switch (ch) { + case ' ': + case '\t': + case '\f': + if (quoteChar == 0) { + return sb.toString(); + } + sb.append((char) ch); + break; + + case '\n': + case '\r': + return sb.toString(); + + case '\'': + case '"': + if (quoteChar == 0) { + quoteChar = (char) ch; + } else if (quoteChar == ch) { + quoteChar = 0; + } else { + sb.append((char) ch); + } + break; + + case '\\': + if (quoteChar != 0) { + ch = in.read(); + switch (ch) { + case '\n': + case '\r': + while (ch == ' ' || ch == '\n' + || ch == '\r' || ch == '\t' + || ch == '\f') { + ch = in.read(); + } + continue; + + case 'n': + ch = '\n'; + break; + case 'r': + ch = '\r'; + break; + case 't': + ch = '\t'; + break; + case 'f': + ch = '\f'; + break; + } + } + sb.append((char) ch); + break; + + default: + sb.append((char) ch); + } + + ch = in.read(); + } + + return sb.toString(); + } + + void skipWhite() throws IOException { + while (ch != -1) { + switch (ch) { + case ' ': + case '\t': + case '\n': + case '\r': + case '\f': + break; + + case '#': + ch = in.read(); + while (ch != '\n' && ch != '\r' && ch != -1) { + ch = in.read(); + } + break; + + default: + return; + } + + ch = in.read(); + } + } + } +} diff --git a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/main/Main.java b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/main/Main.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/main/Main.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2011, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.main; + +import jdk.incubator.jpackage.internal.Arguments; +import jdk.incubator.jpackage.internal.Log; +import jdk.incubator.jpackage.internal.CLIHelp; +import java.io.PrintWriter; +import java.util.ResourceBundle; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.text.MessageFormat; + +public class Main { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.MainResources"); + + /** + * main(String... args) + * This is the entry point for the jpackage tool. + * + * @param args command line arguments + */ + public static void main(String... args) throws Exception { + // Create logger with default system.out and system.err + Log.setLogger(null); + + int status = new jdk.incubator.jpackage.main.Main().execute(args); + System.exit(status); + } + + /** + * execute() - this is the entry point for the ToolProvider API. + * + * @param out output stream + * @param err error output stream + * @param args command line arguments + * @return an exit code. 0 means success, non-zero means an error occurred. + */ + public int execute(PrintWriter out, PrintWriter err, String... args) { + // Create logger with provided streams + Log.Logger logger = new Log.Logger(); + logger.setPrintWriter(out, err); + Log.setLogger(logger); + + return execute(args); + } + + private int execute(String... args) { + try { + String[] newArgs; + try { + newArgs = CommandLine.parse(args); + } catch (FileNotFoundException fnfe) { + Log.error(MessageFormat.format(I18N.getString( + "ERR_CannotParseOptions"), fnfe.getMessage())); + return 1; + } catch (IOException ioe) { + Log.error(ioe.getMessage()); + return 1; + } + + if (newArgs.length == 0) { + CLIHelp.showHelp(true); + } else if (hasHelp(newArgs)){ + if (hasVersion(newArgs)) { + Log.info(System.getProperty("java.version") + "\n"); + } + CLIHelp.showHelp(false); + } else if (hasVersion(newArgs)) { + Log.info(System.getProperty("java.version")); + } else { + Arguments arguments = new Arguments(newArgs); + if (!arguments.processArguments()) { + // processArguments() will log error message if failed. + return 1; + } + } + return 0; + } finally { + Log.flush(); + } + } + + private boolean hasHelp(String[] args) { + for (String a : args) { + if ("--help".equals(a) || "-h".equals(a)) { + return true; + } + } + return false; + } + + private boolean hasVersion(String[] args) { + for (String a : args) { + if ("--version".equals(a)) { + return true; + } + } + return false; + } + +} diff --git a/src/jdk.incubator.jpackage/share/classes/module-info.java b/src/jdk.incubator.jpackage/share/classes/module-info.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/classes/module-info.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * Defines the Java Packaging tool, jpackage. + * + *

jpackage is a tool for generating self-contained application bundles. + * + *

This module provides the equivalent of command-line access to jpackage + * via the {@link java.util.spi.ToolProvider ToolProvider} SPI. + * Instances of the tool can be obtained by calling + * {@link java.util.spi.ToolProvider#findFirst ToolProvider.findFirst} + * or the {@link java.util.ServiceLoader service loader} with the name + * {@code "jpackage"}. + * + * @implNote The {@code jpackage} tool is not thread-safe. An application + * should not call either of the + * {@link java.util.spi.ToolProvider ToolProvider} {@code run} methods + * concurrently, even with separate {@code "jpackage"} {@code ToolProvider} + * instances, or undefined behavior may result. + * + * + * @moduleGraph + * @since 14 + */ + +module jdk.incubator.jpackage { + requires jdk.jlink; + + requires java.desktop; + + uses jdk.incubator.jpackage.internal.Bundler; + uses jdk.incubator.jpackage.internal.Bundlers; + + provides jdk.incubator.jpackage.internal.Bundlers with + jdk.incubator.jpackage.internal.BasicBundlers; + + provides java.util.spi.ToolProvider + with jdk.incubator.jpackage.internal.JPackageToolProvider; +} diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/FileAttributes.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/FileAttributes.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/FileAttributes.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef FILEATTRIBUTES_H +#define FILEATTRIBUTES_H + +#include "Platform.h" +#include "PlatformString.h" +#include "FileAttribute.h" + +#include + +class FileAttributes { +private: + TString FFileName; + bool FFollowLink; + std::vector FAttributes; + + bool WriteAttributes(); + bool ReadAttributes(); + bool Valid(const FileAttribute Value); + +public: + FileAttributes(const TString FileName, bool FollowLink = true); + + void Append(const FileAttribute Value); + bool Contains(const FileAttribute Value); + void Remove(const FileAttribute Value); +}; + +#endif // FILEATTRIBUTES_H + diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/FilePath.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/FilePath.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/FilePath.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef FILEPATH_H +#define FILEPATH_H + +#include "Platform.h" +#include "PlatformString.h" +#include "FileAttribute.h" + +#include + +class FileAttributes { +private: + TString FFileName; + bool FFollowLink; + std::vector FAttributes; + + bool WriteAttributes(); + bool ReadAttributes(); + bool Valid(const FileAttribute Value); + +public: + FileAttributes(const TString FileName, bool FollowLink = true); + + void Append(const FileAttribute Value); + bool Contains(const FileAttribute Value); + void Remove(const FileAttribute Value); +}; + +class FilePath { +private: + FilePath(void) {} + ~FilePath(void) {} + +public: + static bool FileExists(const TString FileName); + static bool DirectoryExists(const TString DirectoryName); + + static bool DeleteFile(const TString FileName); + static bool DeleteDirectory(const TString DirectoryName); + + static TString ExtractFilePath(TString Path); + static TString ExtractFileExt(TString Path); + static TString ExtractFileName(TString Path); + static TString ChangeFileExt(TString Path, TString Extension); + + static TString IncludeTrailingSeparator(const TString value); + static TString IncludeTrailingSeparator(const char* value); + static TString IncludeTrailingSeparator(const wchar_t* value); + static TString FixPathForPlatform(TString Path); + static TString FixPathSeparatorForPlatform(TString Path); + static TString PathSeparator(); + + static bool CreateDirectory(TString Path, bool ownerOnly); + static void ChangePermissions(TString FileName, bool ownerOnly); +}; + +#endif //FILEPATH_H diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Helpers.cpp b/src/jdk.incubator.jpackage/share/native/libapplauncher/Helpers.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Helpers.cpp @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Helpers.h" +#include "PlatformString.h" +#include "PropertyFile.h" + + +bool Helpers::SplitOptionIntoNameValue( + TString option, TString& Name, TString& Value) { + bool hasValue = false; + Name = _T(""); + Value = _T(""); + unsigned int index = 0; + + for (; index < option.length(); index++) { + TCHAR c = option[index]; + + switch (c) { + case '=': { + index++; + hasValue = true; + break; + } + + case '\\': { + if (index + 1 < option.length()) { + c = option[index + 1]; + + switch (c) { + case '\\': { + index++; + Name += '\\'; + break; + } + + case '=': { + index++; + Name += '='; + break; + } + } + + } + + continue; + } + + default: { + Name += c; + continue; + } + } + + break; + } + + if (hasValue) { + Value = option.substr(index, index - option.length()); + } + + return (option.length() > 0); +} + + +TString Helpers::ReplaceString(TString subject, const TString& search, + const TString& replace) { + size_t pos = 0; + while((pos = subject.find(search, pos)) != TString::npos) { + subject.replace(pos, search.length(), replace); + pos += replace.length(); + } + return subject; +} + +TString Helpers::ConvertIdToFilePath(TString Value) { + TString search; + search = '.'; + TString replace; + replace = '/'; + TString result = ReplaceString(Value, search, replace); + return result; +} + +TString Helpers::ConvertIdToJavaPath(TString Value) { + TString search; + search = '.'; + TString replace; + replace = '/'; + TString result = ReplaceString(Value, search, replace); + search = '\\'; + result = ReplaceString(result, search, replace); + return result; +} + +TString Helpers::ConvertJavaPathToId(TString Value) { + TString search; + search = '/'; + TString replace; + replace = '.'; + TString result = ReplaceString(Value, search, replace); + return result; +} + +OrderedMap + Helpers::GetJavaOptionsFromConfig(IPropertyContainer* config) { + OrderedMap result; + + for (unsigned int index = 0; index < config->GetCount(); index++) { + TString argname = + TString(_T("jvmarg.")) + PlatformString(index + 1).toString(); + TString argvalue; + + if (config->GetValue(argname, argvalue) == false) { + break; + } + else if (argvalue.empty() == false) { + TString name; + TString value; + if (Helpers::SplitOptionIntoNameValue(argvalue, name, value)) { + result.Append(name, value); + } + } + } + + return result; +} + +std::list Helpers::GetArgsFromConfig(IPropertyContainer* config) { + std::list result; + + for (unsigned int index = 0; index < config->GetCount(); index++) { + TString argname = TString(_T("arg.")) + + PlatformString(index + 1).toString(); + TString argvalue; + + if (config->GetValue(argname, argvalue) == false) { + break; + } + else if (argvalue.empty() == false) { + result.push_back((argvalue)); + } + } + + return result; +} + +std::list + Helpers::MapToNameValueList(OrderedMap Map) { + std::list result; + std::vector keys = Map.GetKeys(); + + for (OrderedMap::const_iterator iterator = Map.begin(); + iterator != Map.end(); iterator++) { + JPPair *item = *iterator; + TString key = item->first; + TString value = item->second; + + if (value.length() == 0) { + result.push_back(key); + } else { + result.push_back(key + _T('=') + value); + } + } + + return result; +} + +TString Helpers::NameValueToString(TString name, TString value) { + TString result; + + if (value.empty() == true) { + result = name; + } + else { + result = name + TString(_T("=")) + value; + } + + return result; +} + +std::list Helpers::StringToArray(TString Value) { + std::list result; + TString line; + + for (unsigned int index = 0; index < Value.length(); index++) { + TCHAR c = Value[index]; + + switch (c) { + case '\n': { + result.push_back(line); + line = _T(""); + break; + } + + case '\r': { + result.push_back(line); + line = _T(""); + + if (Value[index + 1] == '\n') + index++; + + break; + } + + default: { + line += c; + } + } + } + + // The buffer may not have ended with a Carriage Return/Line Feed. + if (line.length() > 0) { + result.push_back(line); + } + + return result; +} diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Helpers.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/Helpers.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Helpers.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef HELPERS_H +#define HELPERS_H + +#include "Platform.h" +#include "OrderedMap.h" +#include "IniFile.h" + + +class Helpers { +private: + Helpers(void) {} + ~Helpers(void) {} + +public: + // Supports two formats for option: + // Example 1: + // foo=bar + // + // Example 2: + // + static bool SplitOptionIntoNameValue(TString option, + TString& Name, TString& Value); + static TString ReplaceString(TString subject, const TString& search, + const TString& replace); + static TString ConvertIdToFilePath(TString Value); + static TString ConvertIdToJavaPath(TString Value); + static TString ConvertJavaPathToId(TString Value); + + static OrderedMap + GetJavaOptionsFromConfig(IPropertyContainer* config); + static std::list GetArgsFromConfig(IPropertyContainer* config); + + static std::list + MapToNameValueList(OrderedMap Map); + + static TString NameValueToString(TString name, TString value); + + static std::list StringToArray(TString Value); +}; + +#endif // HELPERS_H diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/IniFile.cpp b/src/jdk.incubator.jpackage/share/native/libapplauncher/IniFile.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/IniFile.cpp @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "IniFile.h" +#include "Helpers.h" + +#include + + +IniFile::IniFile() : ISectionalPropertyContainer() { +} + +IniFile::~IniFile() { + for (OrderedMap::iterator iterator = + FMap.begin(); iterator != FMap.end(); iterator++) { + JPPair *item = *iterator; + delete item->second; + } +} + +bool IniFile::LoadFromFile(const TString FileName) { + bool result = false; + Platform& platform = Platform::GetInstance(); + + std::list contents = platform.LoadFromFile(FileName); + + if (contents.empty() == false) { + bool found = false; + + // Determine the if file is an INI file or property file. + // Assign FDefaultSection if it is + // an INI file. Otherwise FDefaultSection is NULL. + for (std::list::const_iterator iterator = contents.begin(); + iterator != contents.end(); iterator++) { + TString line = *iterator; + + if (line[0] == ';') { + // Semicolon is a comment so ignore the line. + continue; + } + else { + if (line[0] == '[') { + found = true; + } + + break; + } + } + + if (found == true) { + TString sectionName; + + for (std::list::const_iterator iterator = contents.begin(); + iterator != contents.end(); iterator++) { + TString line = *iterator; + + if (line[0] == ';') { + // Semicolon is a comment so ignore the line. + continue; + } + else if (line[0] == '[' && line[line.length() - 1] == ']') { + sectionName = line.substr(1, line.size() - 2); + } + else if (sectionName.empty() == false) { + TString name; + TString value; + + if (Helpers::SplitOptionIntoNameValue( + line, name, value) == true) { + Append(sectionName, name, value); + } + } + } + + result = true; + } + } + + return result; +} + +bool IniFile::SaveToFile(const TString FileName, bool ownerOnly) { + bool result = false; + + std::list contents; + std::vector keys = FMap.GetKeys(); + + for (unsigned int index = 0; index < keys.size(); index++) { + TString name = keys[index]; + IniSectionData *section; + + if (FMap.GetValue(name, section) == true) { + contents.push_back(_T("[") + name + _T("]")); + std::list lines = section->GetLines(); + contents.insert(contents.end(), lines.begin(), lines.end()); + contents.push_back(_T("")); + } + } + + Platform& platform = Platform::GetInstance(); + platform.SaveToFile(FileName, contents, ownerOnly); + result = true; + return result; +} + +void IniFile::Append(const TString SectionName, + const TString Key, TString Value) { + if (FMap.ContainsKey(SectionName) == true) { + IniSectionData* section; + + if (FMap.GetValue(SectionName, section) == true && section != NULL) { + section->SetValue(Key, Value); + } + } + else { + IniSectionData *section = new IniSectionData(); + section->SetValue(Key, Value); + FMap.Append(SectionName, section); + } +} + +void IniFile::AppendSection(const TString SectionName, + OrderedMap Values) { + if (FMap.ContainsKey(SectionName) == true) { + IniSectionData* section; + + if (FMap.GetValue(SectionName, section) == true && section != NULL) { + section->Append(Values); + } + } + else { + IniSectionData *section = new IniSectionData(Values); + FMap.Append(SectionName, section); + } +} + +bool IniFile::GetValue(const TString SectionName, + const TString Key, TString& Value) { + bool result = false; + IniSectionData* section; + + if (FMap.GetValue(SectionName, section) == true && section != NULL) { + result = section->GetValue(Key, Value); + } + + return result; +} + +bool IniFile::SetValue(const TString SectionName, + const TString Key, TString Value) { + bool result = false; + IniSectionData* section; + + if (FMap.GetValue(SectionName, section) && section != NULL) { + result = section->SetValue(Key, Value); + } + else { + Append(SectionName, Key, Value); + } + + + return result; +} + +bool IniFile::GetSection(const TString SectionName, + OrderedMap &Data) { + bool result = false; + + if (FMap.ContainsKey(SectionName) == true) { + IniSectionData* section = NULL; + + if (FMap.GetValue(SectionName, section) == true && section != NULL) { + OrderedMap data = section->GetData(); + Data.Append(data); + result = true; + } + } + + return result; +} + +bool IniFile::ContainsSection(const TString SectionName) { + return FMap.ContainsKey(SectionName); +} + +//---------------------------------------------------------------------------- + +IniSectionData::IniSectionData() { + FMap.SetAllowDuplicates(true); +} + +IniSectionData::IniSectionData(OrderedMap Values) { + FMap = Values; +} + +std::vector IniSectionData::GetKeys() { + return FMap.GetKeys(); +} + +std::list IniSectionData::GetLines() { + std::list result; + std::vector keys = FMap.GetKeys(); + + for (unsigned int index = 0; index < keys.size(); index++) { + TString name = keys[index]; + TString value; + + if (FMap.GetValue(name, value) == true) { + name = Helpers::ReplaceString(name, _T("="), _T("\\=")); + value = Helpers::ReplaceString(value, _T("="), _T("\\=")); + + TString line = name + _T('=') + value; + result.push_back(line); + } + } + + return result; +} + +OrderedMap IniSectionData::GetData() { + OrderedMap result = FMap; + return result; +} + +bool IniSectionData::GetValue(const TString Key, TString& Value) { + return FMap.GetValue(Key, Value); +} + +bool IniSectionData::SetValue(const TString Key, TString Value) { + return FMap.SetValue(Key, Value); +} + +void IniSectionData::Append(OrderedMap Values) { + FMap.Append(Values); +} + +size_t IniSectionData::GetCount() { + return FMap.Count(); +} diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/IniFile.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/IniFile.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/IniFile.h @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef INIFILE_H +#define INIFILE_H + +#include "Platform.h" +#include "OrderedMap.h" + +#include + + +class IniSectionData : public IPropertyContainer { +private: + OrderedMap FMap; + +public: + IniSectionData(); + IniSectionData(OrderedMap Values); + + std::vector GetKeys(); + std::list GetLines(); + OrderedMap GetData(); + + bool SetValue(const TString Key, TString Value); + void Append(OrderedMap Values); + + virtual bool GetValue(const TString Key, TString& Value); + virtual size_t GetCount(); +}; + + +class IniFile : public ISectionalPropertyContainer { +private: + OrderedMap FMap; + +public: + IniFile(); + virtual ~IniFile(); + + void internalTest(); + + bool LoadFromFile(const TString FileName); + bool SaveToFile(const TString FileName, bool ownerOnly = true); + + void Append(const TString SectionName, const TString Key, TString Value); + void AppendSection(const TString SectionName, + OrderedMap Values); + bool SetValue(const TString SectionName, + const TString Key, TString Value); + + // ISectionalPropertyContainer + virtual bool GetSection(const TString SectionName, + OrderedMap &Data); + virtual bool ContainsSection(const TString SectionName); + virtual bool GetValue(const TString SectionName, + const TString Key, TString& Value); +}; + +#endif // INIFILE_H diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/JavaVirtualMachine.cpp b/src/jdk.incubator.jpackage/share/native/libapplauncher/JavaVirtualMachine.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/JavaVirtualMachine.cpp @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "JavaVirtualMachine.h" +#include "Platform.h" +#include "PlatformString.h" +#include "FilePath.h" +#include "Package.h" +#include "Helpers.h" +#include "Messages.h" +#include "Macros.h" + +#include "jni.h" + +#include +#include +#include + + +bool RunVM() { + JavaVirtualMachine javavm; + + bool result = javavm.StartJVM(); + + if (!result) { + Platform& platform = Platform::GetInstance(); + platform.ShowMessage(_T("Failed to launch JVM\n")); + } + + return result; +} + +//---------------------------------------------------------------------------- + +JavaOptions::JavaOptions(): FOptions(NULL) { +} + +JavaOptions::~JavaOptions() { + if (FOptions != NULL) { + for (unsigned int index = 0; index < GetCount(); index++) { + delete[] FOptions[index].optionString; + } + + delete[] FOptions; + } +} + +void JavaOptions::AppendValue(const TString Key, TString Value, void* Extra) { + JavaOptionItem item; + item.name = Key; + item.value = Value; + item.extraInfo = Extra; + FItems.push_back(item); +} + +void JavaOptions::AppendValue(const TString Key, TString Value) { + AppendValue(Key, Value, NULL); +} + +void JavaOptions::AppendValue(const TString Key) { + AppendValue(Key, _T(""), NULL); +} + +void JavaOptions::AppendValues(OrderedMap Values) { + if (Values.GetAllowDuplicates()) { + for (int i = 0; i < (int)Values.Count(); i++) { + TString name, value; + + bool bResult = Values.GetKey(i, name); + bResult &= Values.GetValue(i, value); + + if (bResult) { + AppendValue(name, value); + } + } + } else { // In case we asked to add values from OrderedMap with allow + // duplicates set to false. Not used now, but should avoid possible + // bugs. + std::vector orderedKeys = Values.GetKeys(); + + for (std::vector::const_iterator iterator = orderedKeys.begin(); + iterator != orderedKeys.end(); iterator++) { + TString name = *iterator; + TString value; + + if (Values.GetValue(name, value) == true) { + AppendValue(name, value); + } + } + } +} + +void JavaOptions::ReplaceValue(const TString Key, TString Value) { + for (std::list::iterator iterator = FItems.begin(); + iterator != FItems.end(); iterator++) { + + TString lkey = iterator->name; + + if (lkey == Key) { + JavaOptionItem item = *iterator; + item.value = Value; + iterator = FItems.erase(iterator); + FItems.insert(iterator, item); + break; + } + } +} + +std::list JavaOptions::ToList() { + std::list result; + Macros& macros = Macros::GetInstance(); + + for (std::list::const_iterator iterator = FItems.begin(); + iterator != FItems.end(); iterator++) { + TString key = iterator->name; + TString value = iterator->value; + TString option = Helpers::NameValueToString(key, value); + option = macros.ExpandMacros(option); + result.push_back(option); + } + + return result; +} + +size_t JavaOptions::GetCount() { + return FItems.size(); +} + +//---------------------------------------------------------------------------- + +JavaVirtualMachine::JavaVirtualMachine() { +} + +JavaVirtualMachine::~JavaVirtualMachine(void) { +} + +bool JavaVirtualMachine::StartJVM() { + Platform& platform = Platform::GetInstance(); + Package& package = Package::GetInstance(); + + TString classpath = package.GetClassPath(); + TString modulepath = package.GetModulePath(); + JavaOptions options; + + if (modulepath.empty() == false) { + options.AppendValue(_T("-Djava.module.path"), modulepath); + } + + options.AppendValue(_T("-Djava.library.path"), + package.GetPackageAppDirectory() + FilePath::PathSeparator() + + package.GetPackageLauncherDirectory()); + options.AppendValue( + _T("-Djava.launcher.path"), package.GetPackageLauncherDirectory()); + options.AppendValues(package.GetJavaOptions()); + +#ifdef DEBUG + if (package.Debugging() == dsJava) { + options.AppendValue(_T("-Xdebug"), _T("")); + options.AppendValue( + _T("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=localhost:5005"), + _T("")); + platform.ShowMessage(_T("localhost:5005")); + } +#endif // DEBUG + + TString maxHeapSizeOption; + TString minHeapSizeOption; + + + if (package.GetMemoryState() == PackageBootFields::msAuto) { + TPlatformNumber memorySize = package.GetMemorySize(); + TString memory = + PlatformString((size_t)memorySize).toString() + _T("m"); + maxHeapSizeOption = TString(_T("-Xmx")) + memory; + options.AppendValue(maxHeapSizeOption, _T("")); + + if (memorySize > 256) + minHeapSizeOption = _T("-Xms256m"); + else + minHeapSizeOption = _T("-Xms") + memory; + + options.AppendValue(minHeapSizeOption, _T("")); + } + + TString mainClassName = package.GetMainClassName(); + TString mainModule = package.GetMainModule(); + + if (mainClassName.empty() == true && mainModule.empty() == true) { + Messages& messages = Messages::GetInstance(); + platform.ShowMessage(messages.GetMessage(NO_MAIN_CLASS_SPECIFIED)); + return false; + } + + configureLibrary(); + + // Initialize the arguments to JLI_Launch() + // + // On Mac OS X JLI_Launch spawns a new thread that actually starts the JVM. + // This new thread simply re-runs main(argc, argv). Therefore we do not + // want to add new args if we are still in the original main thread so we + // will treat them as command line args provided by the user ... + // Only propagate original set of args first time. + + options.AppendValue(_T("-classpath")); + options.AppendValue(classpath); + + std::list vmargs; + vmargs.push_back(package.GetCommandName()); + + if (package.HasSplashScreen() == true) { + options.AppendValue(TString(_T("-splash:")) + + package.GetSplashScreenFileName(), _T("")); + } + + if (mainModule.empty() == true) { + options.AppendValue(Helpers::ConvertJavaPathToId(mainClassName), + _T("")); + } else { + options.AppendValue(_T("-m")); + options.AppendValue(mainModule); + } + + return launchVM(options, vmargs); +} + +void JavaVirtualMachine::configureLibrary() { + Platform& platform = Platform::GetInstance(); + Package& package = Package::GetInstance(); + TString libName = package.GetJavaLibraryFileName(); + platform.addPlatformDependencies(&javaLibrary); + javaLibrary.Load(libName); +} + +bool JavaVirtualMachine::launchVM(JavaOptions& options, + std::list& vmargs) { + Platform& platform = Platform::GetInstance(); + Package& package = Package::GetInstance(); + +#ifdef MAC + // Mac adds a ProcessSerialNumber to args when launched from .app + // filter out the psn since they it's not expected in the app + if (platform.IsMainThread() == false) { + std::list loptions = options.ToList(); + vmargs.splice(vmargs.end(), loptions, + loptions.begin(), loptions.end()); + } +#else + std::list loptions = options.ToList(); + vmargs.splice(vmargs.end(), loptions, loptions.begin(), loptions.end()); +#endif + + std::list largs = package.GetArgs(); + vmargs.splice(vmargs.end(), largs, largs.begin(), largs.end()); + + size_t argc = vmargs.size(); + DynamicBuffer argv(argc + 1); + if (argv.GetData() == NULL) { + return false; + } + + unsigned int index = 0; + for (std::list::const_iterator iterator = vmargs.begin(); + iterator != vmargs.end(); iterator++) { + TString item = *iterator; + std::string arg = PlatformString(item).toStdString(); +#ifdef DEBUG + printf("%i %s\n", index, arg.c_str()); +#endif // DEBUG + argv[index] = PlatformString::duplicate(arg.c_str()); + index++; + } + + argv[argc] = NULL; + +// On Mac we can only free the boot fields if the calling thread is +// not the main thread. +#ifdef MAC + if (platform.IsMainThread() == false) { + package.FreeBootFields(); + } +#else + package.FreeBootFields(); +#endif // MAC + + if (javaLibrary.JavaVMCreate(argc, argv.GetData()) == true) { + return true; + } + + for (index = 0; index < argc; index++) { + if (argv[index] != NULL) { + delete[] argv[index]; + } + } + + return false; +} diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/JavaVirtualMachine.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/JavaVirtualMachine.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/JavaVirtualMachine.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef JAVAVIRTUALMACHINE_H +#define JAVAVIRTUALMACHINE_H + + +#include "jni.h" +#include "Platform.h" +#include "Library.h" + +struct JavaOptionItem { + TString name; + TString value; + void* extraInfo; +}; + +class JavaOptions { +private: + std::list FItems; + JavaVMOption* FOptions; + +public: + JavaOptions(); + ~JavaOptions(); + + void AppendValue(const TString Key, TString Value, void* Extra); + void AppendValue(const TString Key, TString Value); + void AppendValue(const TString Key); + void AppendValues(OrderedMap Values); + void ReplaceValue(const TString Key, TString Value); + std::list ToList(); + size_t GetCount(); +}; + +class JavaVirtualMachine { +private: + JavaLibrary javaLibrary; + + void configureLibrary(); + bool launchVM(JavaOptions& options, std::list& vmargs); +public: + JavaVirtualMachine(); + ~JavaVirtualMachine(void); + + bool StartJVM(); +}; + +bool RunVM(); + +#endif // JAVAVIRTUALMACHINE_H diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Library.cpp b/src/jdk.incubator.jpackage/share/native/libapplauncher/Library.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Library.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Library.h" +#include "Platform.h" +#include "Messages.h" +#include "PlatformString.h" + +#include +#include + +Library::Library() { + Initialize(); +} + +Library::Library(const TString &FileName) { + Initialize(); + Load(FileName); +} + +Library::~Library() { + Unload(); +} + +void Library::Initialize() { + FModule = NULL; + FDependentLibraryNames = NULL; + FDependenciesLibraries = NULL; +} + +void Library::InitializeDependencies() { + if (FDependentLibraryNames == NULL) { + FDependentLibraryNames = new std::vector(); + } + + if (FDependenciesLibraries == NULL) { + FDependenciesLibraries = new std::vector(); + } +} + +void Library::LoadDependencies() { + if (FDependentLibraryNames != NULL && FDependenciesLibraries != NULL) { + for (std::vector::const_iterator iterator = + FDependentLibraryNames->begin(); + iterator != FDependentLibraryNames->end(); iterator++) { + Library* library = new Library(); + + if (library->Load(*iterator) == true) { + FDependenciesLibraries->push_back(library); + } + } + + delete FDependentLibraryNames; + FDependentLibraryNames = NULL; + } +} + +void Library::UnloadDependencies() { + if (FDependenciesLibraries != NULL) { + for (std::vector::const_iterator iterator = + FDependenciesLibraries->begin(); + iterator != FDependenciesLibraries->end(); iterator++) { + Library* library = *iterator; + + if (library != NULL) { + library->Unload(); + delete library; + } + } + + delete FDependenciesLibraries; + FDependenciesLibraries = NULL; + } +} + +Procedure Library::GetProcAddress(const std::string& MethodName) const { + Platform& platform = Platform::GetInstance(); + return platform.GetProcAddress(FModule, MethodName); +} + +bool Library::Load(const TString &FileName) { + bool result = true; + + if (FModule == NULL) { + LoadDependencies(); + Platform& platform = Platform::GetInstance(); + FModule = platform.LoadLibrary(FileName); + + if (FModule == NULL) { + Messages& messages = Messages::GetInstance(); + platform.ShowMessage(messages.GetMessage(LIBRARY_NOT_FOUND), + FileName); + result = false; + } else { + fname = PlatformString(FileName).toStdString(); + } + } + + return result; +} + +bool Library::Unload() { + bool result = false; + + if (FModule != NULL) { + Platform& platform = Platform::GetInstance(); + platform.FreeLibrary(FModule); + FModule = NULL; + UnloadDependencies(); + result = true; + } + + return result; +} + +void Library::AddDependency(const TString &FileName) { + InitializeDependencies(); + + if (FDependentLibraryNames != NULL) { + FDependentLibraryNames->push_back(FileName); + } +} + +void Library::AddDependencies(const std::vector &Dependencies) { + if (Dependencies.size() > 0) { + InitializeDependencies(); + + if (FDependentLibraryNames != NULL) { + for (std::vector::const_iterator iterator = + FDependentLibraryNames->begin(); + iterator != FDependentLibraryNames->end(); iterator++) { + TString fileName = *iterator; + AddDependency(fileName); + } + } + } +} + +JavaLibrary::JavaLibrary() : Library(), FCreateProc(NULL) { +} + +bool JavaLibrary::JavaVMCreate(size_t argc, char *argv[]) { + if (FCreateProc == NULL) { + FCreateProc = (JAVA_CREATE) GetProcAddress(LAUNCH_FUNC); + } + + if (FCreateProc == NULL) { + Platform& platform = Platform::GetInstance(); + Messages& messages = Messages::GetInstance(); + platform.ShowMessage( + messages.GetMessage(FAILED_LOCATING_JVM_ENTRY_POINT)); + return false; + } + + return FCreateProc((int) argc, argv, + 0, NULL, + 0, NULL, + "", + "", + "java", + "java", + false, + false, + false, + 0) == 0; +} diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Library.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/Library.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Library.h @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef LIBRARY_H +#define LIBRARY_H + +#include "PlatformDefs.h" +//#include "Platform.h" +#include "OrderedMap.h" + +#include "jni.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +// Private typedef for function pointer casting + +#if defined(_WIN32) && !defined(_WIN64) +#define LAUNCH_FUNC "_JLI_Launch@56" +#else +#define LAUNCH_FUNC "JLI_Launch" +#endif + + +typedef int (JNICALL *JAVA_CREATE)(int argc, char ** argv, + int jargc, const char** jargv, + int appclassc, const char** appclassv, + const char* fullversion, + const char* dotversion, + const char* pname, + const char* lname, + jboolean javaargs, + jboolean cpwildcard, + jboolean javaw, + jint ergo); + +class Library { +private: + std::vector *FDependentLibraryNames; + std::vector *FDependenciesLibraries; + Module FModule; + std::string fname; + + void Initialize(); + void InitializeDependencies(); + void LoadDependencies(); + void UnloadDependencies(); + +public: + void* GetProcAddress(const std::string& MethodName) const; + +public: + Library(); + Library(const TString &FileName); + ~Library(); + + bool Load(const TString &FileName); + bool Unload(); + + const std::string& GetName() const { + return fname; + } + + void AddDependency(const TString &FileName); + void AddDependencies(const std::vector &Dependencies); +}; + +class JavaLibrary : public Library { + JAVA_CREATE FCreateProc; + JavaLibrary(const TString &FileName); +public: + JavaLibrary(); + bool JavaVMCreate(size_t argc, char *argv[]); +}; + +#endif // LIBRARY_H + diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Macros.cpp b/src/jdk.incubator.jpackage/share/native/libapplauncher/Macros.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Macros.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Macros.h" +#include "Package.h" +#include "Helpers.h" + + +Macros::Macros(void) { +} + +Macros::~Macros(void) { +} + +void Macros::Initialize() { + Package& package = Package::GetInstance(); + Macros& macros = Macros::GetInstance(); + + // Public macros. + macros.AddMacro(_T("$ROOTDIR"), package.GetPackageRootDirectory()); + macros.AddMacro(_T("$APPDIR"), package.GetPackageAppDirectory()); + macros.AddMacro(_T("$BINDIR"), package.GetPackageLauncherDirectory()); +} + +Macros& Macros::GetInstance() { + static Macros instance; + return instance; +} + +TString Macros::ExpandMacros(TString Value) { + TString result = Value; + + for (std::map::iterator iterator = FData.begin(); + iterator != FData.end(); + iterator++) { + + TString name = iterator->first; + + if (Value.find(name) != TString::npos) { + TString lvalue = iterator->second; + result = Helpers::ReplaceString(Value, name, lvalue); + result = ExpandMacros(result); + break; + } + } + + return result; +} + +void Macros::AddMacro(TString Key, TString Value) { + FData.insert(std::map::value_type(Key, Value)); +} diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Macros.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/Macros.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Macros.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef MACROS_H +#define MACROS_H + +#include "Platform.h" + +#include + + +class Macros { +private: + std::map FData; + + Macros(void); + +public: + static Macros& GetInstance(); + static void Initialize(); + ~Macros(void); + + TString ExpandMacros(TString Value); + void AddMacro(TString Key, TString Value); +}; + +#endif // MACROS_H diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Messages.cpp b/src/jdk.incubator.jpackage/share/native/libapplauncher/Messages.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Messages.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Messages.h" +#include "Platform.h" +#include "FilePath.h" +#include "Helpers.h" +#include "Macros.h" +#include "JavaVirtualMachine.h" + +Messages::Messages(void) { + FMessages.SetReadOnly(false); + FMessages.SetValue(LIBRARY_NOT_FOUND, _T("Failed to find library.")); + FMessages.SetValue(FAILED_CREATING_JVM, _T("Failed to create JVM")); + FMessages.SetValue(FAILED_LOCATING_JVM_ENTRY_POINT, + _T("Failed to locate JLI_Launch")); + FMessages.SetValue(NO_MAIN_CLASS_SPECIFIED, _T("No main class specified")); + FMessages.SetValue(METHOD_NOT_FOUND, _T("No method %s in class %s.")); + FMessages.SetValue(CLASS_NOT_FOUND, _T("Class %s not found.")); + FMessages.SetValue(ERROR_INVOKING_METHOD, _T("Error invoking method.")); + FMessages.SetValue(APPCDS_CACHE_FILE_NOT_FOUND, + _T("Error: AppCDS cache does not exists:\n%s\n")); +} + +Messages& Messages::GetInstance() { + static Messages instance; + // Guaranteed to be destroyed. Instantiated on first use. + return instance; +} + +Messages::~Messages(void) { +} + +TString Messages::GetMessage(const TString Key) { + TString result; + FMessages.GetValue(Key, result); + Macros& macros = Macros::GetInstance(); + result = macros.ExpandMacros(result); + return result; +} diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Messages.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/Messages.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Messages.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef MESSAGES_H +#define MESSAGES_H + +#include "PropertyFile.h" + +#define LIBRARY_NOT_FOUND _T("library.not.found") +#define FAILED_CREATING_JVM _T("failed.creating.jvm") +#define FAILED_LOCATING_JVM_ENTRY_POINT _T("failed.locating.jvm.entry.point") +#define NO_MAIN_CLASS_SPECIFIED _T("no.main.class.specified") + +#define METHOD_NOT_FOUND _T("method.not.found") +#define CLASS_NOT_FOUND _T("class.not.found") +#define ERROR_INVOKING_METHOD _T("error.invoking.method") + +#define CONFIG_FILE_NOT_FOUND _T("config.file.not.found") + +#define BUNDLED_JVM_NOT_FOUND _T("bundled.jvm.not.found") + +#define APPCDS_CACHE_FILE_NOT_FOUND _T("appcds.cache.file.not.found") + +class Messages { +private: + PropertyFile FMessages; + + Messages(void); +public: + static Messages& GetInstance(); + ~Messages(void); + + TString GetMessage(const TString Key); +}; + +#endif // MESSAGES_H diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/OrderedMap.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/OrderedMap.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/OrderedMap.h @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef ORDEREDMAP_H +#define ORDEREDMAP_H + +#include +#include +#include +#include + +#include + +template +struct JPPair +{ + typedef _T1 first_type; + typedef _T2 second_type; + + first_type first; + second_type second; + + JPPair(first_type Value1, second_type Value2) { + first = Value1; + second = Value2; + } +}; + + +template +class OrderedMap { +public: + typedef TKey key_type; + typedef TValue mapped_type; + typedef JPPair container_type; + typedef typename std::vector::iterator iterator; + typedef typename std::vector::const_iterator const_iterator; + +private: + typedef std::map map_type; + typedef std::vector list_type; + + map_type FMap; + list_type FList; + bool FAllowDuplicates; + + typename list_type::iterator FindListItem(const key_type Key) { + typename list_type::iterator result = FList.end(); + + for (typename list_type::iterator iterator = + FList.begin(); iterator != FList.end(); iterator++) { + container_type *item = *iterator; + + if (item->first == Key) { + result = iterator; + break; + } + } + + return result; + } + +public: + OrderedMap() { + FAllowDuplicates = false; + } + + OrderedMap(const OrderedMap &Value) { + Append(Value); + FAllowDuplicates = Value.GetAllowDuplicates(); + } + + ~OrderedMap() { + Clear(); + } + + void SetAllowDuplicates(bool Value) { + FAllowDuplicates = Value; + } + + bool GetAllowDuplicates() const { + return FAllowDuplicates; + } + + iterator begin() { + return FList.begin(); + } + + const_iterator begin() const { + return FList.begin(); + } + + iterator end() { + return FList.end(); + } + + const_iterator end() const { + return FList.end(); + } + + void Clear() { + for (typename list_type::iterator iterator = + FList.begin(); iterator != FList.end(); iterator++) { + container_type *item = *iterator; + + if (item != NULL) { + delete item; + item = NULL; + } + } + + FMap.clear(); + FList.clear(); + } + + bool ContainsKey(key_type Key) { + bool result = false; + + if (FMap.find(Key) != FMap.end()) { + result = true; + } + + return result; + } + + std::vector GetKeys() { + std::vector result; + + for (typename list_type::const_iterator iterator = FList.begin(); + iterator != FList.end(); iterator++) { + container_type *item = *iterator; + result.push_back(item->first); + } + + return result; + } + + void Assign(const OrderedMap &Value) { + Clear(); + Append(Value); + } + + void Append(const OrderedMap &Value) { + for (size_t index = 0; index < Value.FList.size(); index++) { + container_type *item = Value.FList[index]; + Append(item->first, item->second); + } + } + + void Append(key_type Key, mapped_type Value) { + container_type *item = new container_type(Key, Value); + FMap.insert(std::pair(Key, item)); + FList.push_back(item); + } + + bool RemoveByKey(key_type Key) { + bool result = false; + typename list_type::iterator iterator = FindListItem(Key); + + if (iterator != FList.end()) { + FMap.erase(Key); + FList.erase(iterator); + result = true; + } + + return result; + } + + bool GetValue(key_type Key, mapped_type &Value) { + bool result = false; + container_type* item = FMap[Key]; + + if (item != NULL) { + Value = item->second; + result = true; + } + + return result; + } + + bool SetValue(key_type Key, mapped_type &Value) { + bool result = false; + + if ((FAllowDuplicates == false) && (ContainsKey(Key) == true)) { + container_type *item = FMap[Key]; + + if (item != NULL) { + item->second = Value; + result = true; + } + } + else { + Append(Key, Value); + result = true; + } + + return result; + } + + bool GetKey(int index, key_type &Value) { + if (index < 0 || index >= (int)FList.size()) { + return false; + } + container_type *item = FList.at(index); + if (item != NULL) { + Value = item->first; + return true; + } + + return false; + } + + bool GetValue(int index, mapped_type &Value) { + if (index < 0 || index >= (int)FList.size()) { + return false; + } + container_type *item = FList.at(index); + if (item != NULL) { + Value = item->second; + return true; + } + + return false; + } + + mapped_type &operator[](key_type Key) { + container_type* item = FMap[Key]; + assert(item != NULL); + + if (item != NULL) { + return item->second; + } + + throw std::invalid_argument("Key not found"); + } + + OrderedMap& operator= (OrderedMap &Value) { + Clear(); + FAllowDuplicates = Value.GetAllowDuplicates(); + Append(Value); + return *this; + } + + size_t Count() { + return FList.size(); + } +}; + +#endif // ORDEREDMAP_H diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Package.cpp b/src/jdk.incubator.jpackage/share/native/libapplauncher/Package.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Package.cpp @@ -0,0 +1,557 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Package.h" +#include "Helpers.h" +#include "Macros.h" +#include "IniFile.h" + +#include + + +Package::Package(void) { + FInitialized = false; + Initialize(); +} + +TPlatformNumber StringToPercentageOfNumber(TString Value, + TPlatformNumber Number) { + TPlatformNumber result = 0; + size_t percentage = atoi(PlatformString(Value.c_str())); + + if (percentage > 0 && Number > 0) { + result = Number * percentage / 100; + } + + return result; +} + +void Package::Initialize() { + if (FInitialized == true) { + return; + } + + Platform& platform = Platform::GetInstance(); + + FBootFields = new PackageBootFields(); + FDebugging = dsNone; + + // Allow duplicates for Java options, so we can have multiple --add-exports + // or similar args. + FBootFields->FJavaOptions.SetAllowDuplicates(true); + FBootFields->FPackageRootDirectory = platform.GetPackageRootDirectory(); + FBootFields->FPackageAppDirectory = platform.GetPackageAppDirectory(); + FBootFields->FPackageLauncherDirectory = + platform.GetPackageLauncherDirectory(); + FBootFields->FAppDataDirectory = platform.GetAppDataDirectory(); + + std::map keys = platform.GetKeys(); + + // Read from configure.cfg/Info.plist + AutoFreePtr config = + platform.GetConfigFile(platform.GetConfigFileName()); + + config->GetValue(keys[CONFIG_SECTION_APPLICATION], + keys[JPACKAGE_APP_DATA_DIR], FBootFields->FPackageAppDataDirectory); + FBootFields->FPackageAppDataDirectory = + FilePath::FixPathForPlatform(FBootFields->FPackageAppDataDirectory); + + // Main JAR. + config->GetValue(keys[CONFIG_SECTION_APPLICATION], + keys[CONFIG_MAINJAR_KEY], FBootFields->FMainJar); + FBootFields->FMainJar = FilePath::FixPathForPlatform(FBootFields->FMainJar); + + // Main Module. + config->GetValue(keys[CONFIG_SECTION_APPLICATION], + keys[CONFIG_MAINMODULE_KEY], FBootFields->FMainModule); + + // Classpath. + // 1. If the provided class path contains main jar then only use + // provided class path. + // 2. If class path provided by config file is empty then add main jar. + // 3. If main jar is not in provided class path then add it. + config->GetValue(keys[CONFIG_SECTION_APPLICATION], + keys[CONFIG_CLASSPATH_KEY], FBootFields->FClassPath); + FBootFields->FClassPath = + FilePath::FixPathSeparatorForPlatform(FBootFields->FClassPath); + + if (FBootFields->FClassPath.empty() == true) { + FBootFields->FClassPath = GetMainJar(); + } else if (FBootFields->FClassPath.find(GetMainJar()) == TString::npos) { + FBootFields->FClassPath = GetMainJar() + + FilePath::PathSeparator() + FBootFields->FClassPath; + } + + // Modulepath. + config->GetValue(keys[CONFIG_SECTION_APPLICATION], + keys[CONFIG_MODULEPATH_KEY], FBootFields->FModulePath); + FBootFields->FModulePath = + FilePath::FixPathSeparatorForPlatform(FBootFields->FModulePath); + + // Main Class. + config->GetValue(keys[CONFIG_SECTION_APPLICATION], + keys[CONFIG_MAINCLASSNAME_KEY], FBootFields->FMainClassName); + + // Splash Screen. + if (config->GetValue(keys[CONFIG_SECTION_APPLICATION], + keys[CONFIG_SPLASH_KEY], + FBootFields->FSplashScreenFileName) == true) { + FBootFields->FSplashScreenFileName = + FilePath::IncludeTrailingSeparator(GetPackageAppDirectory()) + + FilePath::FixPathForPlatform(FBootFields->FSplashScreenFileName); + + if (FilePath::FileExists(FBootFields->FSplashScreenFileName) == false) { + FBootFields->FSplashScreenFileName = _T(""); + } + } + + // Runtime. + config->GetValue(keys[CONFIG_SECTION_APPLICATION], + keys[JAVA_RUNTIME_KEY], FBootFields->FJavaRuntimeDirectory); + + // Read jvmargs. + PromoteAppCDSState(config); + ReadJavaOptions(config); + + // Read args if none were passed in. + if (FBootFields->FArgs.size() == 0) { + OrderedMap args; + + if (config->GetSection(keys[CONFIG_SECTION_ARGOPTIONS], args) == true) { + FBootFields->FArgs = Helpers::MapToNameValueList(args); + } + } + + // Auto Memory. + TString autoMemory; + + if (config->GetValue(keys[CONFIG_SECTION_APPLICATION], + keys[CONFIG_APP_MEMORY], autoMemory) == true) { + if (autoMemory == _T("auto") || autoMemory == _T("100%")) { + FBootFields->FMemoryState = PackageBootFields::msAuto; + FBootFields->FMemorySize = platform.GetMemorySize(); + } else if (autoMemory.length() == 2 && isdigit(autoMemory[0]) && + autoMemory[1] == '%') { + FBootFields->FMemoryState = PackageBootFields::msAuto; + FBootFields->FMemorySize = + StringToPercentageOfNumber(autoMemory.substr(0, 1), + platform.GetMemorySize()); + } else if (autoMemory.length() == 3 && isdigit(autoMemory[0]) && + isdigit(autoMemory[1]) && autoMemory[2] == '%') { + FBootFields->FMemoryState = PackageBootFields::msAuto; + FBootFields->FMemorySize = + StringToPercentageOfNumber(autoMemory.substr(0, 2), + platform.GetMemorySize()); + } else { + FBootFields->FMemoryState = PackageBootFields::msManual; + FBootFields->FMemorySize = 0; + } + } + + // Debug + TString debug; + if (config->GetValue(keys[CONFIG_SECTION_APPLICATION], + keys[CONFIG_APP_DEBUG], debug) == true) { + FBootFields->FArgs.push_back(debug); + } +} + +void Package::Clear() { + FreeBootFields(); + FInitialized = false; +} + +// This is the only location that the AppCDS state should be modified except +// by command line arguments provided by the user. +// +// The state of AppCDS is as follows: +// +// -> cdsUninitialized +// -> cdsGenCache If -Xappcds:generatecache +// -> cdsDisabled If -Xappcds:off +// -> cdsEnabled If "AppCDSJavaOptions" section is present +// -> cdsAuto If "AppCDSJavaOptions" section is present and +// app.appcds.cache=auto +// -> cdsDisabled Default +// +void Package::PromoteAppCDSState(ISectionalPropertyContainer* Config) { + Platform& platform = Platform::GetInstance(); + std::map keys = platform.GetKeys(); + + // The AppCDS state can change at this point. + switch (platform.GetAppCDSState()) { + case cdsEnabled: + case cdsAuto: + case cdsDisabled: + case cdsGenCache: { + // Do nothing. + break; + } + + case cdsUninitialized: { + if (Config->ContainsSection( + keys[CONFIG_SECTION_APPCDSJAVAOPTIONS]) == true) { + // If the AppCDS section is present then enable AppCDS. + TString appCDSCacheValue; + + // If running with AppCDS enabled, and the configuration has + // been setup so "auto" is enabled, then + // the launcher will attempt to generate the cache file + // automatically and run the application. + if (Config->GetValue(keys[CONFIG_SECTION_APPLICATION], + _T("app.appcds.cache"), appCDSCacheValue) == true && + appCDSCacheValue == _T("auto")) { + platform.SetAppCDSState(cdsAuto); + } + else { + platform.SetAppCDSState(cdsEnabled); + } + } else { + + platform.SetAppCDSState(cdsDisabled); + } + } + } +} + +void Package::ReadJavaOptions(ISectionalPropertyContainer* Config) { + Platform& platform = Platform::GetInstance(); + std::map keys = platform.GetKeys(); + + // Evaluate based on the current AppCDS state. + switch (platform.GetAppCDSState()) { + case cdsUninitialized: { + throw Exception(_T("Internal Error")); + } + + case cdsDisabled: { + Config->GetSection(keys[CONFIG_SECTION_JAVAOPTIONS], + FBootFields->FJavaOptions); + break; + } + + case cdsGenCache: { + Config->GetSection(keys[ + CONFIG_SECTION_APPCDSGENERATECACHEJAVAOPTIONS], + FBootFields->FJavaOptions); + break; + } + + case cdsAuto: + case cdsEnabled: { + if (Config->GetValue(keys[CONFIG_SECTION_APPCDSJAVAOPTIONS], + _T( "-XX:SharedArchiveFile"), + FBootFields->FAppCDSCacheFileName) == true) { + // File names may contain the incorrect path separators. + // The cache file name must be corrected at this point. + if (FBootFields->FAppCDSCacheFileName.empty() == false) { + IniFile* iniConfig = dynamic_cast(Config); + + if (iniConfig != NULL) { + FBootFields->FAppCDSCacheFileName = + FilePath::FixPathForPlatform( + FBootFields->FAppCDSCacheFileName); + iniConfig->SetValue(keys[ + CONFIG_SECTION_APPCDSJAVAOPTIONS], + _T( "-XX:SharedArchiveFile"), + FBootFields->FAppCDSCacheFileName); + } + } + + Config->GetSection(keys[CONFIG_SECTION_APPCDSJAVAOPTIONS], + FBootFields->FJavaOptions); + } + + break; + } + } +} + +void Package::SetCommandLineArguments(int argc, TCHAR* argv[]) { + if (argc > 0) { + std::list args; + + // Prepare app arguments. Skip value at index 0 - + // this is path to executable. + FBootFields->FCommandName = argv[0]; + + // Path to executable is at 0 index so start at index 1. + for (int index = 1; index < argc; index++) { + TString arg = argv[index]; + +#ifdef DEBUG + if (arg == _T("-debug")) { + FDebugging = dsNative; + } + + if (arg == _T("-javadebug")) { + FDebugging = dsJava; + } +#endif //DEBUG +#ifdef MAC + if (arg.find(_T("-psn_"), 0) != TString::npos) { + Platform& platform = Platform::GetInstance(); + + if (platform.IsMainThread() == true) { +#ifdef DEBUG + printf("%s\n", arg.c_str()); +#endif //DEBUG + continue; + } + } + + if (arg == _T("-NSDocumentRevisionsDebugMode")) { + // Ignore -NSDocumentRevisionsDebugMode and + // the following YES/NO + index++; + continue; + } +#endif //MAC + + args.push_back(arg); + } + + if (args.size() > 0) { + FBootFields->FArgs = args; + } + } +} + +Package& Package::GetInstance() { + static Package instance; + // Guaranteed to be destroyed. Instantiated on first use. + return instance; +} + +Package::~Package(void) { + FreeBootFields(); +} + +void Package::FreeBootFields() { + if (FBootFields != NULL) { + delete FBootFields; + FBootFields = NULL; + } +} + +OrderedMap Package::GetJavaOptions() { + return FBootFields->FJavaOptions; +} + +std::vector GetKeysThatAreNotDuplicates(OrderedMap &Defaults, OrderedMap &Overrides) { + std::vector result; + std::vector overrideKeys = Overrides.GetKeys(); + + for (size_t index = 0; index < overrideKeys.size(); index++) { + TString overridesKey = overrideKeys[index]; + TString overridesValue; + TString defaultValue; + + if ((Defaults.ContainsKey(overridesKey) == false) || + (Defaults.GetValue(overridesKey, defaultValue) == true && + Overrides.GetValue(overridesKey, overridesValue) == true && + defaultValue != overridesValue)) { + result.push_back(overridesKey); + } + } + + return result; +} + +OrderedMap CreateOrderedMapFromKeyList(OrderedMap &Map, std::vector &Keys) { + OrderedMap result; + + for (size_t index = 0; index < Keys.size(); index++) { + TString key = Keys[index]; + TString value; + + if (Map.GetValue(key, value) == true) { + result.Append(key, value); + } + } + + return result; +} + +std::vector GetKeysThatAreNotOverridesOfDefaultValues( + OrderedMap &Defaults, OrderedMap &Overrides) { + std::vector result; + std::vector keys = Overrides.GetKeys(); + + for (unsigned int index = 0; index< keys.size(); index++) { + TString key = keys[index]; + + if (Defaults.ContainsKey(key) == true) { + try { + TString value = Overrides[key]; + Defaults[key] = value; + } + catch (std::out_of_range &) { + } + } + else { + result.push_back(key); + } + } + + return result; +} + +std::list Package::GetArgs() { + assert(FBootFields != NULL); + return FBootFields->FArgs; +} + +TString Package::GetPackageRootDirectory() { + assert(FBootFields != NULL); + return FBootFields->FPackageRootDirectory; +} + +TString Package::GetPackageAppDirectory() { + assert(FBootFields != NULL); + return FBootFields->FPackageAppDirectory; +} + +TString Package::GetPackageLauncherDirectory() { + assert(FBootFields != NULL); + return FBootFields->FPackageLauncherDirectory; +} + +TString Package::GetAppDataDirectory() { + assert(FBootFields != NULL); + return FBootFields->FAppDataDirectory; +} + +TString Package::GetAppCDSCacheDirectory() { + if (FAppCDSCacheDirectory.empty()) { + Platform& platform = Platform::GetInstance(); + FAppCDSCacheDirectory = FilePath::IncludeTrailingSeparator( + platform.GetAppDataDirectory()) + + FilePath::IncludeTrailingSeparator( + GetPackageAppDataDirectory()) + _T("cache"); + + Macros& macros = Macros::GetInstance(); + FAppCDSCacheDirectory = macros.ExpandMacros(FAppCDSCacheDirectory); + FAppCDSCacheDirectory = + FilePath::FixPathForPlatform(FAppCDSCacheDirectory); + } + + return FAppCDSCacheDirectory; +} + +TString Package::GetAppCDSCacheFileName() { + assert(FBootFields != NULL); + + if (FBootFields->FAppCDSCacheFileName.empty() == false) { + Macros& macros = Macros::GetInstance(); + FBootFields->FAppCDSCacheFileName = + macros.ExpandMacros(FBootFields->FAppCDSCacheFileName); + FBootFields->FAppCDSCacheFileName = + FilePath::FixPathForPlatform(FBootFields->FAppCDSCacheFileName); + } + + return FBootFields->FAppCDSCacheFileName; +} + +TString Package::GetPackageAppDataDirectory() { + assert(FBootFields != NULL); + return FBootFields->FPackageAppDataDirectory; +} + +TString Package::GetClassPath() { + assert(FBootFields != NULL); + return FBootFields->FClassPath; +} + +TString Package::GetModulePath() { + assert(FBootFields != NULL); + return FBootFields->FModulePath; +} + +TString Package::GetMainJar() { + assert(FBootFields != NULL); + return FBootFields->FMainJar; +} + +TString Package::GetMainModule() { + assert(FBootFields != NULL); + return FBootFields->FMainModule; +} + +TString Package::GetMainClassName() { + assert(FBootFields != NULL); + return FBootFields->FMainClassName; +} + +TString Package::GetJavaLibraryFileName() { + assert(FBootFields != NULL); + + if (FBootFields->FJavaLibraryFileName.empty() == true) { + Platform& platform = Platform::GetInstance(); + Macros& macros = Macros::GetInstance(); + TString jvmRuntimePath = macros.ExpandMacros(GetJavaRuntimeDirectory()); + FBootFields->FJavaLibraryFileName = + platform.GetBundledJavaLibraryFileName(jvmRuntimePath); + } + + return FBootFields->FJavaLibraryFileName; +} + +TString Package::GetJavaRuntimeDirectory() { + assert(FBootFields != NULL); + return FBootFields->FJavaRuntimeDirectory; +} + +TString Package::GetSplashScreenFileName() { + assert(FBootFields != NULL); + return FBootFields->FSplashScreenFileName; +} + +bool Package::HasSplashScreen() { + assert(FBootFields != NULL); + return FilePath::FileExists(FBootFields->FSplashScreenFileName); +} + +TString Package::GetCommandName() { + assert(FBootFields != NULL); + return FBootFields->FCommandName; +} + +TPlatformNumber Package::GetMemorySize() { + assert(FBootFields != NULL); + return FBootFields->FMemorySize; +} + +PackageBootFields::MemoryState Package::GetMemoryState() { + assert(FBootFields != NULL); + return FBootFields->FMemoryState; +} + +DebugState Package::Debugging() { + return FDebugging; +} diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Package.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/Package.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Package.h @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef PACKAGE_H +#define PACKAGE_H + + +#include "Platform.h" +#include "PlatformString.h" +#include "FilePath.h" +#include "PropertyFile.h" + +#include +#include + +class PackageBootFields { +public: + enum MemoryState {msManual, msAuto}; + +public: + OrderedMap FJavaOptions; + std::list FArgs; + + TString FPackageRootDirectory; + TString FPackageAppDirectory; + TString FPackageLauncherDirectory; + TString FAppDataDirectory; + TString FPackageAppDataDirectory; + TString FClassPath; + TString FModulePath; + TString FMainJar; + TString FMainModule; + TString FMainClassName; + TString FJavaRuntimeDirectory; + TString FJavaLibraryFileName; + TString FSplashScreenFileName; + bool FUseJavaPreferences; + TString FCommandName; + + TString FAppCDSCacheFileName; + + TPlatformNumber FMemorySize; + MemoryState FMemoryState; +}; + + +class Package { +private: + Package(Package const&); // Don't Implement. + void operator=(Package const&); // Don't implement + +private: + bool FInitialized; + PackageBootFields* FBootFields; + TString FAppCDSCacheDirectory; + + DebugState FDebugging; + + Package(void); + + TString GetMainJar(); + void ReadJavaOptions(ISectionalPropertyContainer* Config); + void PromoteAppCDSState(ISectionalPropertyContainer* Config); + +public: + static Package& GetInstance(); + ~Package(void); + + void Initialize(); + void Clear(); + void FreeBootFields(); + + void SetCommandLineArguments(int argc, TCHAR* argv[]); + + OrderedMap GetJavaOptions(); + TString GetMainModule(); + + std::list GetArgs(); + + TString GetPackageRootDirectory(); + TString GetPackageAppDirectory(); + TString GetPackageLauncherDirectory(); + TString GetAppDataDirectory(); + + TString GetAppCDSCacheDirectory(); + TString GetAppCDSCacheFileName(); + + TString GetPackageAppDataDirectory(); + TString GetClassPath(); + TString GetModulePath(); + TString GetMainClassName(); + TString GetJavaLibraryFileName(); + TString GetJavaRuntimeDirectory(); + TString GetSplashScreenFileName(); + bool HasSplashScreen(); + TString GetCommandName(); + + TPlatformNumber GetMemorySize(); + PackageBootFields::MemoryState GetMemoryState(); + + DebugState Debugging(); +}; + +#endif // PACKAGE_H diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Platform.cpp b/src/jdk.incubator.jpackage/share/native/libapplauncher/Platform.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Platform.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Platform.h" +#include "Messages.h" +#include "PlatformString.h" +#include "FilePath.h" + +#include +#include + +#ifdef WINDOWS +#include "WindowsPlatform.h" +#endif // WINDOWS +#ifdef LINUX +#include "LinuxPlatform.h" +#endif // LINUX +#ifdef MAC +#include "MacPlatform.h" +#endif // MAC + +Platform& Platform::GetInstance() { +#ifdef WINDOWS + static WindowsPlatform instance; +#endif // WINDOWS + +#ifdef LINUX + static LinuxPlatform instance; +#endif // LINUX + +#ifdef MAC + static MacPlatform instance; +#endif // MAC + + return instance; +} + +TString Platform::GetConfigFileName() { + TString result; + TString basedir = GetPackageAppDirectory(); + + if (basedir.empty() == false) { + basedir = FilePath::IncludeTrailingSeparator(basedir); + TString appConfig = basedir + GetAppName() + _T(".cfg"); + + if (FilePath::FileExists(appConfig) == true) { + result = appConfig; + } + else { + result = basedir + _T("package.cfg"); + + if (FilePath::FileExists(result) == false) { + result = _T(""); + } + } + } + + return result; +} + +std::list Platform::LoadFromFile(TString FileName) { + std::list result; + + if (FilePath::FileExists(FileName) == true) { + std::wifstream stream(FileName.data()); + InitStreamLocale(&stream); + + if (stream.is_open() == true) { + while (stream.eof() == false) { + std::wstring line; + std::getline(stream, line); + + // # at the first character will comment out the line. + if (line.empty() == false && line[0] != '#') { + result.push_back(PlatformString(line).toString()); + } + } + } + } + + return result; +} + +void Platform::SaveToFile(TString FileName, std::list Contents, bool ownerOnly) { + TString path = FilePath::ExtractFilePath(FileName); + + if (FilePath::DirectoryExists(path) == false) { + FilePath::CreateDirectory(path, ownerOnly); + } + + std::wofstream stream(FileName.data()); + InitStreamLocale(&stream); + + FilePath::ChangePermissions(FileName.data(), ownerOnly); + + if (stream.is_open() == true) { + for (std::list::const_iterator iterator = + Contents.begin(); iterator != Contents.end(); iterator++) { + TString line = *iterator; + stream << PlatformString(line).toUnicodeString() << std::endl; + } + } +} + +std::map Platform::GetKeys() { + std::map keys; + keys.insert(std::map::value_type(CONFIG_VERSION, + _T("app.version"))); + keys.insert(std::map::value_type(CONFIG_MAINJAR_KEY, + _T("app.mainjar"))); + keys.insert(std::map::value_type(CONFIG_MAINMODULE_KEY, + _T("app.mainmodule"))); + keys.insert(std::map::value_type(CONFIG_MAINCLASSNAME_KEY, + _T("app.mainclass"))); + keys.insert(std::map::value_type(CONFIG_CLASSPATH_KEY, + _T("app.classpath"))); + keys.insert(std::map::value_type(CONFIG_MODULEPATH_KEY, + _T("app.modulepath"))); + keys.insert(std::map::value_type(APP_NAME_KEY, + _T("app.name"))); + keys.insert(std::map::value_type(JAVA_RUNTIME_KEY, + _T("app.runtime"))); + keys.insert(std::map::value_type(JPACKAGE_APP_DATA_DIR, + _T("app.identifier"))); + keys.insert(std::map::value_type(CONFIG_SPLASH_KEY, + _T("app.splash"))); + keys.insert(std::map::value_type(CONFIG_APP_MEMORY, + _T("app.memory"))); + keys.insert(std::map::value_type(CONFIG_APP_DEBUG, + _T("app.debug"))); + keys.insert(std::map::value_type(CONFIG_APPLICATION_INSTANCE, + _T("app.application.instance"))); + keys.insert(std::map::value_type(CONFIG_SECTION_APPLICATION, + _T("Application"))); + keys.insert(std::map::value_type(CONFIG_SECTION_JAVAOPTIONS, + _T("JavaOptions"))); + keys.insert(std::map::value_type(CONFIG_SECTION_APPCDSJAVAOPTIONS, + _T("AppCDSJavaOptions"))); + keys.insert(std::map::value_type(CONFIG_SECTION_APPCDSGENERATECACHEJAVAOPTIONS, + _T("AppCDSGenerateCacheJavaOptions"))); + keys.insert(std::map::value_type(CONFIG_SECTION_ARGOPTIONS, + _T("ArgOptions"))); + + return keys; +} diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Platform.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/Platform.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Platform.h @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef PLATFORM_H +#define PLATFORM_H + +#include "PlatformDefs.h" +#include "Properties.h" +#include "OrderedMap.h" +#include "Library.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +// Config file sections +#define CONFIG_SECTION_APPLICATION _T("CONFIG_SECTION_APPLICATION") +#define CONFIG_SECTION_JAVAOPTIONS _T("CONFIG_SECTION_JAVAOPTIONS") +#define CONFIG_SECTION_APPCDSJAVAOPTIONS _T("CONFIG_SECTION_APPCDSJAVAOPTIONS") +#define CONFIG_SECTION_ARGOPTIONS _T("CONFIG_SECTION_ARGOPTIONS") +#define CONFIG_SECTION_APPCDSGENERATECACHEJAVAOPTIONS \ + _T("CONFIG_SECTION_APPCDSGENERATECACHEJAVAOPTIONS") + +// Config file keys. +#define CONFIG_VERSION _T("CONFIG_VERSION") +#define CONFIG_MAINJAR_KEY _T("CONFIG_MAINJAR_KEY") +#define CONFIG_MAINMODULE_KEY _T("CONFIG_MAINMODULE_KEY") +#define CONFIG_MAINCLASSNAME_KEY _T("CONFIG_MAINCLASSNAME_KEY") +#define CONFIG_CLASSPATH_KEY _T("CONFIG_CLASSPATH_KEY") +#define CONFIG_MODULEPATH_KEY _T("CONFIG_MODULEPATH_KEY") +#define APP_NAME_KEY _T("APP_NAME_KEY") +#define CONFIG_SPLASH_KEY _T("CONFIG_SPLASH_KEY") +#define CONFIG_APP_MEMORY _T("CONFIG_APP_MEMORY") +#define CONFIG_APP_DEBUG _T("CONFIG_APP_DEBUG") +#define CONFIG_APPLICATION_INSTANCE _T("CONFIG_APPLICATION_INSTANCE") + +#define JAVA_RUNTIME_KEY _T("JAVA_RUNTIME_KEY") +#define JPACKAGE_APP_DATA_DIR _T("CONFIG_APP_IDENTIFIER") + +struct WideString { + size_t length; + wchar_t* data; + + WideString() { length = 0; data = NULL; } +}; + +struct MultibyteString { + size_t length; + char* data; + + MultibyteString() { length = 0; data = NULL; } +}; + +class Process { +protected: + std::list FOutput; + +public: + Process() { + Output.SetInstance(this); + Input.SetInstance(this); + } + + virtual ~Process() {} + + virtual bool IsRunning() = 0; + virtual bool Terminate() = 0; + virtual bool Execute(const TString Application, + const std::vector Arguments, bool AWait = false) = 0; + virtual bool Wait() = 0; + virtual TProcessID GetProcessID() = 0; + + virtual std::list GetOutput() { return FOutput; } + virtual void SetInput(TString Value) = 0; + + ReadProperty, &Process::GetOutput> Output; + WriteProperty Input; +}; + + +template +class AutoFreePtr { +private: + T* FObject; + +public: + AutoFreePtr() { + FObject = NULL; + } + + AutoFreePtr(T* Value) { + FObject = Value; + } + + ~AutoFreePtr() { + if (FObject != NULL) { + delete FObject; + } + } + + operator T* () const { + return FObject; + } + + T& operator* () const { + return *FObject; + } + + T* operator->() const { + return FObject; + } + + T** operator&() { + return &FObject; + } + + T* operator=(const T * rhs) { + FObject = rhs; + return FObject; + } +}; + +enum DebugState {dsNone, dsNative, dsJava}; +enum MessageResponse {mrOK, mrCancel}; +enum AppCDSState {cdsUninitialized, cdsDisabled, + cdsEnabled, cdsAuto, cdsGenCache}; + +class Platform { +private: + AppCDSState FAppCDSState; + +protected: + Platform(void): FAppCDSState(cdsUninitialized) { + } + +public: + AppCDSState GetAppCDSState() { return FAppCDSState; } + void SetAppCDSState(AppCDSState Value) { FAppCDSState = Value; } + + static Platform& GetInstance(); + + virtual ~Platform(void) {} + +public: + virtual void ShowMessage(TString title, TString description) = 0; + virtual void ShowMessage(TString description) = 0; + virtual MessageResponse ShowResponseMessage(TString title, + TString description) = 0; + + // Caller must free result using delete[]. + virtual TCHAR* ConvertStringToFileSystemString(TCHAR* Source, + bool &release) = 0; + + // Caller must free result using delete[]. + virtual TCHAR* ConvertFileSystemStringToString(TCHAR* Source, + bool &release) = 0; + + // Returns: + // Windows=C:\Users\\AppData\Local + // Linux=~/.local + // Mac=~/Library/Application Support + virtual TString GetAppDataDirectory() = 0; + + virtual TString GetPackageAppDirectory() = 0; + virtual TString GetPackageLauncherDirectory() = 0; + virtual TString GetPackageRuntimeBinDirectory() = 0; + virtual TString GetAppName() = 0; + + virtual TString GetConfigFileName(); + + virtual TString GetBundledJavaLibraryFileName(TString RuntimePath) = 0; + + // Caller must free result. + virtual ISectionalPropertyContainer* GetConfigFile(TString FileName) = 0; + + virtual TString GetModuleFileName() = 0; + virtual TString GetPackageRootDirectory() = 0; + + virtual Module LoadLibrary(TString FileName) = 0; + virtual void FreeLibrary(Module Module) = 0; + virtual Procedure GetProcAddress(Module Module, std::string MethodName) = 0; + + // Caller must free result. + virtual Process* CreateProcess() = 0; + + virtual bool IsMainThread() = 0; + + // Returns megabytes. + virtual TPlatformNumber GetMemorySize() = 0; + + virtual std::map GetKeys(); + + virtual void InitStreamLocale(wios *stream) = 0; + virtual std::list LoadFromFile(TString FileName); + virtual void SaveToFile(TString FileName, + std::list Contents, bool ownerOnly); + + virtual TString GetTempDirectory() = 0; + + virtual void addPlatformDependencies(JavaLibrary *pJavaLibrary) = 0; + +public: + // String helpers + // Caller must free result using delete[]. + static void CopyString(char *Destination, + size_t NumberOfElements, const char *Source); + + // Caller must free result using delete[]. + static void CopyString(wchar_t *Destination, + size_t NumberOfElements, const wchar_t *Source); + + static WideString MultibyteStringToWideString(const char* value); + static MultibyteString WideStringToMultibyteString(const wchar_t* value); +}; + +class Exception: public std::exception { +private: + TString FMessage; + +protected: + void SetMessage(const TString Message) { + FMessage = Message; + } + +public: + explicit Exception() : exception() {} + explicit Exception(const TString Message) : exception() { + SetMessage(Message); + } + virtual ~Exception() throw() {} + + TString GetMessage() { return FMessage; } +}; + +#endif // PLATFORM_H diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/PlatformString.cpp b/src/jdk.incubator.jpackage/share/native/libapplauncher/PlatformString.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/PlatformString.cpp @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "PlatformString.h" + +#include "Helpers.h" + +#include +#include +#include +#include +#include +#include + +#include "jni.h" + +void PlatformString::initialize() { + FWideTStringToFree = NULL; + FLength = 0; + FData = NULL; +} + +PlatformString::PlatformString(void) { + initialize(); +} + +PlatformString::~PlatformString(void) { + if (FData != NULL) { + delete[] FData; + } + + if (FWideTStringToFree != NULL) { + delete[] FWideTStringToFree; + } +} + +PlatformString::PlatformString(const PlatformString &value) { + initialize(); + FLength = value.FLength; + FData = new char[FLength + 1]; + Platform::CopyString(FData, FLength + 1, value.FData); +} + +PlatformString::PlatformString(const char* value) { + initialize(); + FLength = strlen(value); + FData = new char[FLength + 1]; + Platform::CopyString(FData, FLength + 1, value); +} + +PlatformString::PlatformString(size_t Value) { + initialize(); + + std::stringstream ss; + std::string s; + ss << Value; + s = ss.str(); + + FLength = strlen(s.c_str()); + FData = new char[FLength + 1]; + Platform::CopyString(FData, FLength + 1, s.c_str()); +} + +PlatformString::PlatformString(const wchar_t* value) { + initialize(); + MultibyteString temp = Platform::WideStringToMultibyteString(value); + FLength = temp.length; + FData = temp.data; +} + +PlatformString::PlatformString(const std::string &value) { + initialize(); + const char* lvalue = value.data(); + FLength = value.size(); + FData = new char[FLength + 1]; + Platform::CopyString(FData, FLength + 1, lvalue); +} + +PlatformString::PlatformString(const std::wstring &value) { + initialize(); + const wchar_t* lvalue = value.data(); + MultibyteString temp = Platform::WideStringToMultibyteString(lvalue); + FLength = temp.length; + FData = temp.data; +} + +TString PlatformString::Format(const TString value, ...) { + TString result = value; + + va_list arglist; + va_start(arglist, value); + + while (1) { + size_t pos = result.find(_T("%s"), 0); + + if (pos == TString::npos) { + break; + } + else { + TCHAR* arg = va_arg(arglist, TCHAR*); + + if (arg == NULL) { + break; + } + else { + result.replace(pos, StringLength(_T("%s")), arg); + } + } + } + + va_end(arglist); + + return result; +} + +size_t PlatformString::length() { + return FLength; +} + +char* PlatformString::c_str() { + return FData; +} + +char* PlatformString::toMultibyte() { + return FData; +} + +wchar_t* PlatformString::toWideString() { + WideString result = Platform::MultibyteStringToWideString(FData); + + if (result.data != NULL) { + if (FWideTStringToFree != NULL) { + delete [] FWideTStringToFree; + } + + FWideTStringToFree = result.data; + } + + return result.data; +} + +std::wstring PlatformString::toUnicodeString() { + std::wstring result; + wchar_t* data = toWideString(); + + if (FLength != 0 && data != NULL) { + // NOTE: Cleanup of result is handled by PlatformString destructor. + result = data; + } + + return result; +} + +std::string PlatformString::toStdString() { + std::string result; + char* data = toMultibyte(); + + if (FLength > 0 && data != NULL) { + result = data; + } + + return result; +} + +TCHAR* PlatformString::toPlatformString() { +#ifdef _UNICODE + return toWideString(); +#else + return c_str(); +#endif //_UNICODE +} + +TString PlatformString::toString() { +#ifdef _UNICODE + return toUnicodeString(); +#else + return toStdString(); +#endif //_UNICODE +} + +PlatformString::operator char* () { + return c_str(); +} + +PlatformString::operator wchar_t* () { + return toWideString(); +} + +PlatformString::operator std::wstring () { + return toUnicodeString(); +} + +char* PlatformString::duplicate(const char* Value) { + size_t length = strlen(Value); + char* result = new char[length + 1]; + Platform::CopyString(result, length + 1, Value); + return result; +} + +wchar_t* PlatformString::duplicate(const wchar_t* Value) { + size_t length = wcslen(Value); + wchar_t* result = new wchar_t[length + 1]; + Platform::CopyString(result, length + 1, Value); + return result; +} diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/PlatformString.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/PlatformString.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/PlatformString.h @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef PLATFORMSTRING_H +#define PLATFORMSTRING_H + + +#include +#include +#include +#include + +#include "jni.h" +#include "Platform.h" + + +template +class DynamicBuffer { +private: + T* FData; + size_t FSize; + +public: + DynamicBuffer(size_t Size) { + FSize = 0; + FData = NULL; + Resize(Size); + } + + ~DynamicBuffer() { + delete[] FData; + } + + T* GetData() { return FData; } + size_t GetSize() { return FSize; } + + bool Resize(size_t Size) { + FSize = Size; + + if (FData != NULL) { + delete[] FData; + FData = NULL; + } + + if (FSize != 0) { + FData = new T[FSize]; + if (FData != NULL) { + Zero(); + } else { + return false; + } + } + + return true; + } + + void Zero() { + memset(FData, 0, FSize * sizeof(T)); + } + + T& operator[](size_t index) { + return FData[index]; + } +}; + +class PlatformString { +private: + char* FData; // Stored as UTF-8 + size_t FLength; + wchar_t* FWideTStringToFree; + + void initialize(); + +// Prohibit Heap-Based PlatformStrings +private: + static void *operator new(size_t size); + static void operator delete(void *ptr); + +public: + PlatformString(void); + PlatformString(const PlatformString &value); + PlatformString(const char* value); + PlatformString(const wchar_t* value); + PlatformString(const std::string &value); + PlatformString(const std::wstring &value); + PlatformString(size_t Value); + + static TString Format(const TString value, ...); + + ~PlatformString(void); + + size_t length(); + + char* c_str(); + char* toMultibyte(); + wchar_t* toWideString(); + std::wstring toUnicodeString(); + std::string toStdString(); + TCHAR* toPlatformString(); + TString toString(); + + operator char* (); + operator wchar_t* (); + operator std::wstring (); + + // Caller must free result using delete[]. + static char* duplicate(const char* Value); + + // Caller must free result using delete[]. + static wchar_t* duplicate(const wchar_t* Value); +}; + + +#endif // PLATFORMSTRING_H diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/Properties.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/Properties.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/Properties.h @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef PROPERTIES_H +#define PROPERTIES_H + +#include "PlatformDefs.h" +#include "OrderedMap.h" + +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include + +//using namespace std; + +template +class Property { +private: + ObjectType* FObject; + +public: + Property() { + FObject = NULL; + } + + void SetInstance(ObjectType* Value) { + FObject = Value; + } + + // To set the value using the set method. + ValueType operator =(const ValueType& Value) { + assert(FObject != NULL); + (FObject->*setter)(Value); + return Value; + } + + // The Property class is treated as the internal type. + operator ValueType() { + assert(FObject != NULL); + return (FObject->*getter)(); + } +}; + +template +class ReadProperty { +private: + ObjectType* FObject; + +public: + ReadProperty() { + FObject = NULL; + } + + void SetInstance(ObjectType* Value) { + FObject = Value; + } + + // The Property class is treated as the internal type. + operator ValueType() { + assert(FObject != NULL); + return (FObject->*getter)(); + } +}; + +template +class WriteProperty { +private: + ObjectType* FObject; + +public: + WriteProperty() { + FObject = NULL; + } + + void SetInstance(ObjectType* Value) { + FObject = Value; + } + + // To set the value using the set method. + ValueType operator =(const ValueType& Value) { + assert(FObject != NULL); + (FObject->*setter)(Value); + return Value; + } +}; + +template +class StaticProperty { +public: + StaticProperty() { + } + + // To set the value using the set method. + ValueType operator =(const ValueType& Value) { + (*getter)(Value); + return Value; + } + + // The Property class is treated as the internal type which is the getter. + operator ValueType() { + return (*setter)(); + } +}; + +template +class StaticReadProperty { +public: + StaticReadProperty() { + } + + // The Property class is treated as the internal type which is the getter. + operator ValueType() { + return (*getter)(); + } +}; + +template +class StaticWriteProperty { +public: + StaticWriteProperty() { + } + + // To set the value using the set method. + ValueType operator =(const ValueType& Value) { + (*setter)(Value); + return Value; + } +}; + +class IPropertyContainer { +public: + IPropertyContainer(void) {} + virtual ~IPropertyContainer(void) {} + + virtual bool GetValue(const TString Key, TString& Value) = 0; + virtual size_t GetCount() = 0; +}; + +class ISectionalPropertyContainer { +public: + ISectionalPropertyContainer(void) {} + virtual ~ISectionalPropertyContainer(void) {} + + virtual bool GetValue(const TString SectionName, + const TString Key, TString& Value) = 0; + virtual bool ContainsSection(const TString SectionName) = 0; + virtual bool GetSection(const TString SectionName, + OrderedMap &Data) = 0; +}; + +#endif // PROPERTIES_H + diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/PropertyFile.cpp b/src/jdk.incubator.jpackage/share/native/libapplauncher/PropertyFile.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/PropertyFile.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "PropertyFile.h" + +#include "Helpers.h" +#include "FilePath.h" + +#include + + +PropertyFile::PropertyFile(void) : IPropertyContainer() { + FReadOnly = false; + FModified = false; +} + +PropertyFile::PropertyFile(const TString FileName) : IPropertyContainer() { + FReadOnly = true; + FModified = false; + LoadFromFile(FileName); +} + +PropertyFile::PropertyFile(OrderedMap Value) { + FData.Append(Value); +} + +PropertyFile::PropertyFile(PropertyFile &Value) { + FData = Value.FData; + FReadOnly = Value.FReadOnly; + FModified = Value.FModified; +} + +PropertyFile::~PropertyFile(void) { + FData.Clear(); +} + +void PropertyFile::SetModified(bool Value) { + FModified = Value; +} + +bool PropertyFile::IsModified() { + return FModified; +} + +bool PropertyFile::GetReadOnly() { + return FReadOnly; +} + +void PropertyFile::SetReadOnly(bool Value) { + FReadOnly = Value; +} + +bool PropertyFile::LoadFromFile(const TString FileName) { + bool result = false; + Platform& platform = Platform::GetInstance(); + + std::list contents = platform.LoadFromFile(FileName); + + if (contents.empty() == false) { + for (std::list::const_iterator iterator = contents.begin(); + iterator != contents.end(); iterator++) { + TString line = *iterator; + TString name; + TString value; + + if (Helpers::SplitOptionIntoNameValue(line, name, value) == true) { + FData.Append(name, value); + } + } + + SetModified(false); + result = true; + } + + return result; +} + +bool PropertyFile::SaveToFile(const TString FileName, bool ownerOnly) { + bool result = false; + + if (GetReadOnly() == false && IsModified()) { + std::list contents; + std::vector keys = FData.GetKeys(); + + for (size_t index = 0; index < keys.size(); index++) { + TString name = keys[index]; + + try { + TString value;// = FData[index]; + + if (FData.GetValue(name, value) == true) { + TString line = name + _T('=') + value; + contents.push_back(line); + } + } + catch (std::out_of_range &) { + } + } + + Platform& platform = Platform::GetInstance(); + platform.SaveToFile(FileName, contents, ownerOnly); + + SetModified(false); + result = true; + } + + return result; +} + +bool PropertyFile::GetValue(const TString Key, TString& Value) { + return FData.GetValue(Key, Value); +} + +bool PropertyFile::SetValue(const TString Key, TString Value) { + bool result = false; + + if (GetReadOnly() == false) { + FData.SetValue(Key, Value); + SetModified(true); + result = true; + } + + return result; +} + +bool PropertyFile::RemoveKey(const TString Key) { + bool result = false; + + if (GetReadOnly() == false) { + result = FData.RemoveByKey(Key); + + if (result == true) { + SetModified(true); + } + } + + return result; +} + +size_t PropertyFile::GetCount() { + return FData.Count(); +} + +OrderedMap PropertyFile::GetData() { + return FData; +} diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/PropertyFile.h b/src/jdk.incubator.jpackage/share/native/libapplauncher/PropertyFile.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/PropertyFile.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef PROPERTYFILE_H +#define PROPERTYFILE_H + +#include "Platform.h" +#include "Helpers.h" + + +class PropertyFile : public IPropertyContainer { +private: + bool FReadOnly; + bool FModified; + OrderedMap FData; + + void SetModified(bool Value); + +public: + PropertyFile(void); + PropertyFile(const TString FileName); + PropertyFile(OrderedMap Value); + PropertyFile(PropertyFile &Value); + virtual ~PropertyFile(void); + + bool IsModified(); + bool GetReadOnly(); + void SetReadOnly(bool Value); + + bool LoadFromFile(const TString FileName); + bool SaveToFile(const TString FileName, bool ownerOnly = true); + + bool SetValue(const TString Key, TString Value); + bool RemoveKey(const TString Key); + + OrderedMap GetData(); + + // IPropertyContainer + virtual bool GetValue(const TString Key, TString& Value); + virtual size_t GetCount(); +}; + +#endif // PROPERTYFILE_H diff --git a/src/jdk.incubator.jpackage/share/native/libapplauncher/main.cpp b/src/jdk.incubator.jpackage/share/native/libapplauncher/main.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/share/native/libapplauncher/main.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Platform.h" +#include "PlatformString.h" +#include "FilePath.h" +#include "PropertyFile.h" +#include "JavaVirtualMachine.h" +#include "Package.h" +#include "Macros.h" +#include "Messages.h" + +#include +#include +#include + +/* +This is the app launcher program for application packaging on Windows, Mac, + and Linux. + +Basic approach: + - Launcher (jpackageapplauncher) is executable that loads + applauncher.dll/libapplauncher.dylib/libapplauncher.so + and calls start_launcher below. + - Reads app/package.cfg or Info.plist or app/.cfg for application + launch configuration (package.cfg is property file). + - Load Java with requested Java settings (bundled client Java if availble, + server or installed Java otherwise). + - Wait for Java to exit and then exit from Main + - To debug application by passing command line argument. + - Application folder is added to the library path (so LoadLibrary()) works. + +Limitations and future work: + - Running Java code in primordial thread may cause problems + (example: can not use custom stack size). + Solution used by java launcher is to create a new thread to invoke Java. + See CR 6316197 for more information. +*/ + +extern "C" { + + JNIEXPORT bool start_launcher(int argc, TCHAR* argv[]) { + bool result = false; + bool parentProcess = true; + + // Platform must be initialize first. + Platform& platform = Platform::GetInstance(); + + try { + for (int index = 0; index < argc; index++) { + TString argument = argv[index]; + + if (argument == _T("-Xappcds:generatecache")) { + platform.SetAppCDSState(cdsGenCache); + } + else if (argument == _T("-Xappcds:off")) { + platform.SetAppCDSState(cdsDisabled); + } + else if (argument == _T("-Xapp:child")) { + parentProcess = false; + } + } + + // Package must be initialized after Platform is fully initialized. + Package& package = Package::GetInstance(); + Macros::Initialize(); + package.SetCommandLineArguments(argc, argv); + + switch (platform.GetAppCDSState()) { + case cdsDisabled: + case cdsUninitialized: + case cdsEnabled: { + break; + } + + case cdsGenCache: { + TString cacheDirectory = package.GetAppCDSCacheDirectory(); + + if (FilePath::DirectoryExists(cacheDirectory) == false) { + FilePath::CreateDirectory(cacheDirectory, true); + } else { + TString cacheFileName = + package.GetAppCDSCacheFileName(); + if (FilePath::FileExists(cacheFileName) == true) { + FilePath::DeleteFile(cacheFileName); + } + } + + break; + } + + case cdsAuto: { + TString cacheFileName = package.GetAppCDSCacheFileName(); + + if (parentProcess == true && + FilePath::FileExists(cacheFileName) == false) { + AutoFreePtr process = platform.CreateProcess(); + std::vector args; + args.push_back(_T("-Xappcds:generatecache")); + args.push_back(_T("-Xapp:child")); + process->Execute( + platform.GetModuleFileName(), args, true); + + if (FilePath::FileExists(cacheFileName) == false) { + // Cache does not exist after trying to generate it, + // so run without cache. + platform.SetAppCDSState(cdsDisabled); + package.Clear(); + package.Initialize(); + } + } + + break; + } + } + + // Validation + switch (platform.GetAppCDSState()) { + case cdsDisabled: + case cdsGenCache: { + // Do nothing. + break; + } + + case cdsEnabled: + case cdsAuto: { + TString cacheFileName = + package.GetAppCDSCacheFileName(); + + if (FilePath::FileExists(cacheFileName) == false) { + Messages& messages = Messages::GetInstance(); + TString message = PlatformString::Format( + messages.GetMessage( + APPCDS_CACHE_FILE_NOT_FOUND), + cacheFileName.data()); + throw Exception(message); + } + break; + } + + case cdsUninitialized: { + platform.ShowMessage(_T("Internal Error")); + break; + } + } + + // Run App + result = RunVM(); + } catch (Exception &e) { + platform.ShowMessage(e.GetMessage()); + } + + return result; + } + + JNIEXPORT void stop_launcher() { + } +} diff --git a/src/jdk.incubator.jpackage/unix/native/libapplauncher/FileAttribute.h b/src/jdk.incubator.jpackage/unix/native/libapplauncher/FileAttribute.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/unix/native/libapplauncher/FileAttribute.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef FILEATTRIBUTE_H +#define FILEATTRIBUTE_H + +enum FileAttribute { + faBlockSpecial, + faCharacterSpecial, + faFIFOSpecial, + faNormal, + faDirectory, + faSymbolicLink, + faSocket, + + // Owner + faReadOnly, + faWriteOnly, + faReadWrite, + faExecute, + + // Group + faGroupReadOnly, + faGroupWriteOnly, + faGroupReadWrite, + faGroupExecute, + + // Others + faOthersReadOnly, + faOthersWriteOnly, + faOthersReadWrite, + faOthersExecute, + + faHidden +}; + +#endif // FILEATTRIBUTE_H diff --git a/src/jdk.incubator.jpackage/unix/native/libapplauncher/FileAttributes.cpp b/src/jdk.incubator.jpackage/unix/native/libapplauncher/FileAttributes.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/unix/native/libapplauncher/FileAttributes.cpp @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "FileAttributes.h" + +#include +#include +#include + +FileAttributes::FileAttributes(const TString FileName, bool FollowLink) { + FFileName = FileName; + FFollowLink = FollowLink; + ReadAttributes(); +} + +bool FileAttributes::WriteAttributes() { + bool result = false; + + mode_t attributes = 0; + + for (std::vector::const_iterator iterator = + FAttributes.begin(); + iterator != FAttributes.end(); iterator++) { + switch (*iterator) { + case faBlockSpecial: + { + attributes |= S_IFBLK; + break; + } + case faCharacterSpecial: + { + attributes |= S_IFCHR; + break; + } + case faFIFOSpecial: + { + attributes |= S_IFIFO; + break; + } + case faNormal: + { + attributes |= S_IFREG; + break; + } + case faDirectory: + { + attributes |= S_IFDIR; + break; + } + case faSymbolicLink: + { + attributes |= S_IFLNK; + break; + } + case faSocket: + { + attributes |= S_IFSOCK; + break; + } + + // Owner + case faReadOnly: + { + attributes |= S_IRUSR; + break; + } + case faWriteOnly: + { + attributes |= S_IWUSR; + break; + } + case faReadWrite: + { + attributes |= S_IRUSR; + attributes |= S_IWUSR; + break; + } + case faExecute: + { + attributes |= S_IXUSR; + break; + } + + // Group + case faGroupReadOnly: + { + attributes |= S_IRGRP; + break; + } + case faGroupWriteOnly: + { + attributes |= S_IWGRP; + break; + } + case faGroupReadWrite: + { + attributes |= S_IRGRP; + attributes |= S_IWGRP; + break; + } + case faGroupExecute: + { + attributes |= S_IXGRP; + break; + } + + // Others + case faOthersReadOnly: + { + attributes |= S_IROTH; + break; + } + case faOthersWriteOnly: + { + attributes |= S_IWOTH; + break; + } + case faOthersReadWrite: + { + attributes |= S_IROTH; + attributes |= S_IWOTH; + break; + } + case faOthersExecute: + { + attributes |= S_IXOTH; + break; + } + default: + break; + } + } + + if (chmod(FFileName.data(), attributes) == 0) { + result = true; + } + + return result; +} + +#define S_ISRUSR(m) (((m) & S_IRWXU) == S_IRUSR) +#define S_ISWUSR(m) (((m) & S_IRWXU) == S_IWUSR) +#define S_ISXUSR(m) (((m) & S_IRWXU) == S_IXUSR) + +#define S_ISRGRP(m) (((m) & S_IRWXG) == S_IRGRP) +#define S_ISWGRP(m) (((m) & S_IRWXG) == S_IWGRP) +#define S_ISXGRP(m) (((m) & S_IRWXG) == S_IXGRP) + +#define S_ISROTH(m) (((m) & S_IRWXO) == S_IROTH) +#define S_ISWOTH(m) (((m) & S_IRWXO) == S_IWOTH) +#define S_ISXOTH(m) (((m) & S_IRWXO) == S_IXOTH) + +bool FileAttributes::ReadAttributes() { + bool result = false; + + struct stat status; + + if (stat(StringToFileSystemString(FFileName), &status) == 0) { + result = true; + + if (S_ISBLK(status.st_mode) != 0) { + FAttributes.push_back(faBlockSpecial); + } + if (S_ISCHR(status.st_mode) != 0) { + FAttributes.push_back(faCharacterSpecial); + } + if (S_ISFIFO(status.st_mode) != 0) { + FAttributes.push_back(faFIFOSpecial); + } + if (S_ISREG(status.st_mode) != 0) { + FAttributes.push_back(faNormal); + } + if (S_ISDIR(status.st_mode) != 0) { + FAttributes.push_back(faDirectory); + } + if (S_ISLNK(status.st_mode) != 0) { + FAttributes.push_back(faSymbolicLink); + } + if (S_ISSOCK(status.st_mode) != 0) { + FAttributes.push_back(faSocket); + } + + // Owner + if (S_ISRUSR(status.st_mode) != 0) { + if (S_ISWUSR(status.st_mode) != 0) { + FAttributes.push_back(faReadWrite); + } else { + FAttributes.push_back(faReadOnly); + } + } else if (S_ISWUSR(status.st_mode) != 0) { + FAttributes.push_back(faWriteOnly); + } + + if (S_ISXUSR(status.st_mode) != 0) { + FAttributes.push_back(faExecute); + } + + // Group + if (S_ISRGRP(status.st_mode) != 0) { + if (S_ISWGRP(status.st_mode) != 0) { + FAttributes.push_back(faGroupReadWrite); + } else { + FAttributes.push_back(faGroupReadOnly); + } + } else if (S_ISWGRP(status.st_mode) != 0) { + FAttributes.push_back(faGroupWriteOnly); + } + + if (S_ISXGRP(status.st_mode) != 0) { + FAttributes.push_back(faGroupExecute); + } + + + // Others + if (S_ISROTH(status.st_mode) != 0) { + if (S_ISWOTH(status.st_mode) != 0) { + FAttributes.push_back(faOthersReadWrite); + } else { + FAttributes.push_back(faOthersReadOnly); + } + } else if (S_ISWOTH(status.st_mode) != 0) { + FAttributes.push_back(faOthersWriteOnly); + } + + if (S_ISXOTH(status.st_mode) != 0) { + FAttributes.push_back(faOthersExecute); + } + + if (FFileName.size() > 0 && FFileName[0] == '.') { + FAttributes.push_back(faHidden); + } + } + + return result; +} + +bool FileAttributes::Valid(const FileAttribute Value) { + bool result = false; + + switch (Value) { + case faReadWrite: + case faWriteOnly: + case faExecute: + + case faGroupReadWrite: + case faGroupWriteOnly: + case faGroupReadOnly: + case faGroupExecute: + + case faOthersReadWrite: + case faOthersWriteOnly: + case faOthersReadOnly: + case faOthersExecute: + + case faReadOnly: + result = true; + break; + + default: + break; + } + + return result; +} + +void FileAttributes::Append(FileAttribute Value) { + if (Valid(Value) == true) { + if ((Value == faReadOnly && Contains(faWriteOnly) == true) || + (Value == faWriteOnly && Contains(faReadOnly) == true)) { + Value = faReadWrite; + } + + FAttributes.push_back(Value); + WriteAttributes(); + } +} + +bool FileAttributes::Contains(FileAttribute Value) { + bool result = false; + + std::vector::const_iterator iterator = + std::find(FAttributes.begin(), FAttributes.end(), Value); + + if (iterator != FAttributes.end()) { + result = true; + } + + return result; +} + +void FileAttributes::Remove(FileAttribute Value) { + if (Valid(Value) == true) { + if (Value == faReadOnly && Contains(faReadWrite) == true) { + Append(faWriteOnly); + Remove(faReadWrite); + } else if (Value == faWriteOnly && Contains(faReadWrite) == true) { + Append(faReadOnly); + Remove(faReadWrite); + } + + std::vector::iterator iterator = + std::find(FAttributes.begin(), FAttributes.end(), Value); + + if (iterator != FAttributes.end()) { + FAttributes.erase(iterator); + WriteAttributes(); + } + } +} diff --git a/src/jdk.incubator.jpackage/unix/native/libapplauncher/FilePath.cpp b/src/jdk.incubator.jpackage/unix/native/libapplauncher/FilePath.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/unix/native/libapplauncher/FilePath.cpp @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "PlatformDefs.h" +#include "FilePath.h" + +#include +#include +#include + +bool FilePath::FileExists(const TString FileName) { + bool result = false; + struct stat buf; + + if ((stat(StringToFileSystemString(FileName), &buf) == 0) && + (S_ISREG(buf.st_mode) != 0)) { + result = true; + } + + return result; +} + +bool FilePath::DirectoryExists(const TString DirectoryName) { + bool result = false; + + struct stat buf; + + if ((stat(StringToFileSystemString(DirectoryName), &buf) == 0) && + (S_ISDIR(buf.st_mode) != 0)) { + result = true; + } + + return result; +} + +bool FilePath::DeleteFile(const TString FileName) { + bool result = false; + + if (FileExists(FileName) == true) { + if (unlink(StringToFileSystemString(FileName)) == 0) { + result = true; + } + } + + return result; +} + +bool FilePath::DeleteDirectory(const TString DirectoryName) { + bool result = false; + + if (DirectoryExists(DirectoryName) == true) { + if (unlink(StringToFileSystemString(DirectoryName)) == 0) { + result = true; + } + } + + return result; +} + +TString FilePath::IncludeTrailingSeparator(const TString value) { + TString result = value; + + if (value.size() > 0) { + TString::iterator i = result.end(); + i--; + + if (*i != TRAILING_PATHSEPARATOR) { + result += TRAILING_PATHSEPARATOR; + } + } + + return result; +} + +TString FilePath::IncludeTrailingSeparator(const char* value) { + TString lvalue = PlatformString(value).toString(); + return IncludeTrailingSeparator(lvalue); +} + +TString FilePath::IncludeTrailingSeparator(const wchar_t* value) { + TString lvalue = PlatformString(value).toString(); + return IncludeTrailingSeparator(lvalue); +} + +TString FilePath::ExtractFilePath(TString Path) { + return dirname(StringToFileSystemString(Path)); +} + +TString FilePath::ExtractFileExt(TString Path) { + TString result; + size_t dot = Path.find_last_of('.'); + + if (dot != TString::npos) { + result = Path.substr(dot, Path.size() - dot); + } + + return result; +} + +TString FilePath::ExtractFileName(TString Path) { + return basename(StringToFileSystemString(Path)); +} + +TString FilePath::ChangeFileExt(TString Path, TString Extension) { + TString result; + size_t dot = Path.find_last_of('.'); + + if (dot != TString::npos) { + result = Path.substr(0, dot) + Extension; + } + + if (result.empty() == true) { + result = Path; + } + + return result; +} + +TString FilePath::FixPathForPlatform(TString Path) { + TString result = Path; + std::replace(result.begin(), result.end(), + BAD_TRAILING_PATHSEPARATOR, TRAILING_PATHSEPARATOR); + return result; +} + +TString FilePath::FixPathSeparatorForPlatform(TString Path) { + TString result = Path; + std::replace(result.begin(), result.end(), + BAD_PATH_SEPARATOR, PATH_SEPARATOR); + return result; +} + +TString FilePath::PathSeparator() { + TString result; + result = PATH_SEPARATOR; + return result; +} + +bool FilePath::CreateDirectory(TString Path, bool ownerOnly) { + bool result = false; + + std::list paths; + TString lpath = Path; + + while (lpath.empty() == false && DirectoryExists(lpath) == false) { + paths.push_front(lpath); + lpath = ExtractFilePath(lpath); + } + + for (std::list::iterator iterator = paths.begin(); + iterator != paths.end(); iterator++) { + lpath = *iterator; + + mode_t mode = S_IRWXU; + if (!ownerOnly) { + mode |= S_IRWXG | S_IROTH | S_IXOTH; + } + if (mkdir(StringToFileSystemString(lpath), mode) == 0) { + result = true; + } else { + result = false; + break; + } + } + + return result; +} + +void FilePath::ChangePermissions(TString FileName, bool ownerOnly) { + mode_t mode = S_IRWXU; + if (!ownerOnly) { + mode |= S_IRWXG | S_IROTH | S_IXOTH; + } + chmod(FileName.data(), mode); +} diff --git a/src/jdk.incubator.jpackage/unix/native/libapplauncher/PosixPlatform.cpp b/src/jdk.incubator.jpackage/unix/native/libapplauncher/PosixPlatform.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/unix/native/libapplauncher/PosixPlatform.cpp @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "PosixPlatform.h" + +#include "PlatformString.h" +#include "FilePath.h" +#include "Helpers.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +PosixPlatform::PosixPlatform(void) { +} + +PosixPlatform::~PosixPlatform(void) { +} + +TString PosixPlatform::GetTempDirectory() { + struct passwd* pw = getpwuid(getuid()); + TString homedir(pw->pw_dir); + homedir += getTmpDirString(); + if (!FilePath::DirectoryExists(homedir)) { + if (!FilePath::CreateDirectory(homedir, false)) { + homedir.clear(); + } + } + + return homedir; +} + +TString PosixPlatform::fixName(const TString& name) { + TString fixedName(name); + const TString chars("?:*<>/\\"); + for (TString::const_iterator it = chars.begin(); it != chars.end(); it++) { + fixedName.erase(std::remove(fixedName.begin(), + fixedName.end(), *it), fixedName.end()); + } + return fixedName; +} + +MessageResponse PosixPlatform::ShowResponseMessage(TString title, + TString description) { + MessageResponse result = mrCancel; + + printf("%s %s (Y/N)\n", PlatformString(title).toPlatformString(), + PlatformString(description).toPlatformString()); + fflush(stdout); + + std::string input; + std::cin >> input; + + if (input == "Y") { + result = mrOK; + } + + return result; +} + +Module PosixPlatform::LoadLibrary(TString FileName) { + return dlopen(StringToFileSystemString(FileName), RTLD_LAZY); +} + +void PosixPlatform::FreeLibrary(Module AModule) { + dlclose(AModule); +} + +Procedure PosixPlatform::GetProcAddress(Module AModule, + std::string MethodName) { + return dlsym(AModule, PlatformString(MethodName)); +} + +Process* PosixPlatform::CreateProcess() { + return new PosixProcess(); +} + +void PosixPlatform::addPlatformDependencies(JavaLibrary *pJavaLibrary) { +} + +void Platform::CopyString(char *Destination, + size_t NumberOfElements, const char *Source) { + strncpy(Destination, Source, NumberOfElements); + + if (NumberOfElements > 0) { + Destination[NumberOfElements - 1] = '\0'; + } +} + +void Platform::CopyString(wchar_t *Destination, + size_t NumberOfElements, const wchar_t *Source) { + wcsncpy(Destination, Source, NumberOfElements); + + if (NumberOfElements > 0) { + Destination[NumberOfElements - 1] = '\0'; + } +} + +// Owner must free the return value. + +MultibyteString Platform::WideStringToMultibyteString( + const wchar_t* value) { + MultibyteString result; + size_t count = 0; + + if (value == NULL) { + return result; + } + + count = wcstombs(NULL, value, 0); + if (count > 0) { + result.data = new char[count + 1]; + result.data[count] = '\0'; + result.length = count; + wcstombs(result.data, value, count); + } + + return result; +} + +// Owner must free the return value. + +WideString Platform::MultibyteStringToWideString(const char* value) { + WideString result; + size_t count = 0; + + if (value == NULL) { + return result; + } + + count = mbstowcs(NULL, value, 0); + if (count > 0) { + result.data = new wchar_t[count + 1]; + result.data[count] = '\0'; + result.length = count; + mbstowcs(result.data, value, count); + } + + return result; +} + +void PosixPlatform::InitStreamLocale(wios *stream) { + // Nothing to do for POSIX platforms. +} + +PosixProcess::PosixProcess() : Process() { + FChildPID = 0; + FRunning = false; + FOutputHandle = 0; + FInputHandle = 0; +} + +PosixProcess::~PosixProcess() { + Terminate(); +} + +bool PosixProcess::ReadOutput() { + bool result = false; + + if (FOutputHandle != 0 && IsRunning() == true) { + char buffer[4096] = {0}; + + ssize_t count = read(FOutputHandle, buffer, sizeof (buffer)); + + if (count == -1) { + if (errno == EINTR) { + // continue; + } else { + perror("read"); + exit(1); + } + } else if (count == 0) { + // break; + } else { + std::list output = Helpers::StringToArray(buffer); + FOutput.splice(FOutput.end(), output, output.begin(), output.end()); + result = true; + } + } + + return false; +} + +bool PosixProcess::IsRunning() { + bool result = false; + + if (kill(FChildPID, 0) == 0) { + result = true; + } + + return result; +} + +bool PosixProcess::Terminate() { + bool result = false; + + if (IsRunning() == true && FRunning == true) { + FRunning = false; + Cleanup(); + int status = kill(FChildPID, SIGTERM); + + if (status == 0) { + result = true; + } else { +#ifdef DEBUG + if (errno == EINVAL) { + printf("Kill error: The value of the sig argument is an invalid or unsupported signal number."); + } else if (errno == EPERM) { + printf("Kill error: The process does not have permission to send the signal to any receiving process."); + } else if (errno == ESRCH) { + printf("Kill error: No process or process group can be found corresponding to that specified by pid."); + } +#endif // DEBUG + if (IsRunning() == true) { + status = kill(FChildPID, SIGKILL); + + if (status == 0) { + result = true; + } + } + } + } + + return result; +} + +bool PosixProcess::Wait() { + bool result = false; + + int status = 0; + pid_t wpid = 0; + + wpid = wait(&status); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + if (errno != EINTR) { + status = -1; + } + } + +#ifdef DEBUG + if (WIFEXITED(status)) { + printf("child exited, status=%d\n", WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + printf("child killed (signal %d)\n", WTERMSIG(status)); + } else if (WIFSTOPPED(status)) { + printf("child stopped (signal %d)\n", WSTOPSIG(status)); +#ifdef WIFCONTINUED // Not all implementations support this + } else if (WIFCONTINUED(status)) { + printf("child continued\n"); +#endif // WIFCONTINUED + } else { // Non-standard case -- may never happen + printf("Unexpected status (0x%x)\n", status); + } +#endif // DEBUG + + if (wpid != -1) { + result = true; + } + + return result; +} + +TProcessID PosixProcess::GetProcessID() { + return FChildPID; +} + +void PosixProcess::SetInput(TString Value) { + if (FInputHandle != 0) { + if (write(FInputHandle, Value.data(), Value.size()) < 0) { + throw Exception(_T("Internal Error - write failed")); + } + } +} + +std::list PosixProcess::GetOutput() { + ReadOutput(); + return Process::GetOutput(); +} diff --git a/src/jdk.incubator.jpackage/unix/native/libapplauncher/PosixPlatform.h b/src/jdk.incubator.jpackage/unix/native/libapplauncher/PosixPlatform.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/unix/native/libapplauncher/PosixPlatform.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef POSIXPLATFORM_H +#define POSIXPLATFORM_H + +#include "Platform.h" +#include + +class PosixPlatform : virtual public Platform { +protected: + + TString fixName(const TString& name); + + virtual TString getTmpDirString() = 0; + +public: + PosixPlatform(void); + virtual ~PosixPlatform(void); + +public: + virtual MessageResponse ShowResponseMessage(TString title, + TString description); + + virtual Module LoadLibrary(TString FileName); + virtual void FreeLibrary(Module AModule); + virtual Procedure GetProcAddress(Module AModule, std::string MethodName); + + virtual Process* CreateProcess(); + virtual TString GetTempDirectory(); + void InitStreamLocale(wios *stream); + void addPlatformDependencies(JavaLibrary *pJavaLibrary); +}; + +class PosixProcess : public Process { +private: + pid_t FChildPID; + sigset_t saveblock; + int FOutputHandle; + int FInputHandle; + struct sigaction savintr, savequit; + bool FRunning; + + void Cleanup(); + bool ReadOutput(); + +public: + PosixProcess(); + virtual ~PosixProcess(); + + virtual bool IsRunning(); + virtual bool Terminate(); + virtual bool Execute(const TString Application, + const std::vector Arguments, bool AWait = false); + virtual bool Wait(); + virtual TProcessID GetProcessID(); + virtual void SetInput(TString Value); + virtual std::list GetOutput(); +}; + +#endif // POSIXPLATFORM_H diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinAppBundler.java b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinAppBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinAppBundler.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.*; + +import static jdk.incubator.jpackage.internal.WindowsBundlerParam.*; + +public class WinAppBundler extends AbstractImageBundler { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.WinResources"); + + static final BundlerParamInfo ICON_ICO = + new StandardBundlerParam<>( + "icon.ico", + File.class, + params -> { + File f = ICON.fetchFrom(params); + if (f != null && !f.getName().toLowerCase().endsWith(".ico")) { + Log.error(MessageFormat.format( + I18N.getString("message.icon-not-ico"), f)); + return null; + } + return f; + }, + (s, p) -> new File(s)); + + @Override + public boolean validate(Map params) + throws ConfigException { + try { + Objects.requireNonNull(params); + return doValidate(params); + } catch (RuntimeException re) { + if (re.getCause() instanceof ConfigException) { + throw (ConfigException) re.getCause(); + } else { + throw new ConfigException(re); + } + } + } + + // to be used by chained bundlers, e.g. by EXE bundler to avoid + // skipping validation if p.type does not include "image" + private boolean doValidate(Map p) + throws ConfigException { + + imageBundleValidation(p); + return true; + } + + public boolean bundle(Map p, File outputDirectory) + throws PackagerException { + return doBundle(p, outputDirectory, false) != null; + } + + File doBundle(Map p, File outputDirectory, + boolean dependentTask) throws PackagerException { + if (StandardBundlerParam.isRuntimeInstaller(p)) { + return PREDEFINED_RUNTIME_IMAGE.fetchFrom(p); + } else { + return doAppBundle(p, outputDirectory, dependentTask); + } + } + + File doAppBundle(Map p, File outputDirectory, + boolean dependentTask) throws PackagerException { + try { + File rootDirectory = createRoot(p, outputDirectory, dependentTask, + APP_NAME.fetchFrom(p)); + AbstractAppImageBuilder appBuilder = + new WindowsAppImageBuilder(p, outputDirectory.toPath()); + if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(p) == null ) { + JLinkBundlerHelper.execute(p, appBuilder); + } else { + StandardBundlerParam.copyPredefinedRuntimeImage(p, appBuilder); + } + if (!dependentTask) { + Log.verbose(MessageFormat.format( + I18N.getString("message.result-dir"), + outputDirectory.getAbsolutePath())); + } + return rootDirectory; + } catch (PackagerException pe) { + throw pe; + } catch (Exception e) { + Log.verbose(e); + throw new PackagerException(e); + } + } + + @Override + public String getName() { + return I18N.getString("app.bundler.name"); + } + + @Override + public String getID() { + return "windows.app"; + } + + @Override + public String getBundleType() { + return "IMAGE"; + } + + @Override + public File execute(Map params, + File outputParentDir) throws PackagerException { + return doBundle(params, outputParentDir, false); + } + + @Override + public boolean supported(boolean platformInstaller) { + return true; + } + + @Override + public boolean isDefault() { + return false; + } + +} diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinExeBundler.java b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinExeBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinExeBundler.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.*; + +public class WinExeBundler extends AbstractBundler { + + static { + System.loadLibrary("jpackage"); + } + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.WinResources"); + + public static final BundlerParamInfo APP_BUNDLER + = new WindowsBundlerParam<>( + "win.app.bundler", + WinAppBundler.class, + params -> new WinAppBundler(), + null); + + public static final BundlerParamInfo EXE_IMAGE_DIR + = new WindowsBundlerParam<>( + "win.exe.imageDir", + File.class, + params -> { + File imagesRoot = IMAGES_ROOT.fetchFrom(params); + if (!imagesRoot.exists()) { + imagesRoot.mkdirs(); + } + return new File(imagesRoot, "win-exe.image"); + }, + (s, p) -> null); + + private final static String EXE_WRAPPER_NAME = "msiwrapper.exe"; + + @Override + public String getName() { + return getString("exe.bundler.name"); + } + + @Override + public String getID() { + return "exe"; + } + + @Override + public String getBundleType() { + return "INSTALLER"; + } + + @Override + public File execute(Map params, + File outputParentDir) throws PackagerException { + return bundle(params, outputParentDir); + } + + @Override + public boolean supported(boolean platformInstaller) { + return msiBundler.supported(platformInstaller); + } + + @Override + public boolean isDefault() { + return true; + } + + @Override + public boolean validate(Map params) + throws ConfigException { + return msiBundler.validate(params); + } + + public File bundle(Map params, File outdir) + throws PackagerException { + + IOUtils.writableOutputDir(outdir.toPath()); + + File exeImageDir = EXE_IMAGE_DIR.fetchFrom(params); + + // Write msi to temporary directory. + File msi = msiBundler.bundle(params, exeImageDir); + + try { + new ScriptRunner() + .setDirectory(msi.toPath().getParent()) + .setResourceCategoryId("resource.post-msi-script") + .setScriptNameSuffix("post-msi") + .setEnvironmentVariable("JpMsiFile", msi.getAbsolutePath().toString()) + .run(params); + + return buildEXE(msi, outdir); + } catch (IOException ex) { + Log.verbose(ex); + throw new PackagerException(ex); + } + } + + private File buildEXE(File msi, File outdir) + throws IOException { + + Log.verbose(MessageFormat.format( + getString("message.outputting-to-location"), + outdir.getAbsolutePath())); + + // Copy template msi wrapper next to msi file + String exePath = msi.getAbsolutePath(); + exePath = exePath.substring(0, exePath.lastIndexOf('.')) + ".exe"; + try (InputStream is = OverridableResource.readDefault(EXE_WRAPPER_NAME)) { + Files.copy(is, Path.of(exePath)); + } + // Embed msi in msi wrapper exe. + embedMSI(exePath, msi.getAbsolutePath()); + + Path dstExePath = Paths.get(outdir.getAbsolutePath(), + Path.of(exePath).getFileName().toString()); + Files.deleteIfExists(dstExePath); + + Files.copy(Path.of(exePath), dstExePath); + + Log.verbose(MessageFormat.format( + getString("message.output-location"), + outdir.getAbsolutePath())); + + return dstExePath.toFile(); + } + + private static String getString(String key) + throws MissingResourceException { + return I18N.getString(key); + } + + private final WinMsiBundler msiBundler = new WinMsiBundler(); + + private static native int embedMSI(String exePath, String msiPath); +} diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinMsiBundler.java b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinMsiBundler.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinMsiBundler.java @@ -0,0 +1,580 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.*; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import static jdk.incubator.jpackage.internal.OverridableResource.createResource; +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + +import static jdk.incubator.jpackage.internal.WindowsBundlerParam.*; + +/** + * WinMsiBundler + * + * Produces .msi installer from application image. Uses WiX Toolkit to build + * .msi installer. + *

+ * {@link #execute} method creates a number of source files with the description + * of installer to be processed by WiX tools. Generated source files are stored + * in "config" subdirectory next to "app" subdirectory in the root work + * directory. The following WiX source files are generated: + *

    + *
  • main.wxs. Main source file with the installer description + *
  • bundle.wxf. Source file with application and Java run-time directory tree + * description. + *
+ *

+ * main.wxs file is a copy of main.wxs resource from + * jdk.incubator.jpackage.internal.resources package. It is parametrized with the + * following WiX variables: + *

    + *
  • JpAppName. Name of the application. Set to the value of --name command + * line option + *
  • JpAppVersion. Version of the application. Set to the value of + * --app-version command line option + *
  • JpAppVendor. Vendor of the application. Set to the value of --vendor + * command line option + *
  • JpAppDescription. Description of the application. Set to the value of + * --description command line option + *
  • JpProductCode. Set to product code UUID of the application. Random value + * generated by jpackage every time {@link #execute} method is called + *
  • JpProductUpgradeCode. Set to upgrade code UUID of the application. Random + * value generated by jpackage every time {@link #execute} method is called if + * --win-upgrade-uuid command line option is not specified. Otherwise this + * variable is set to the value of --win-upgrade-uuid command line option + *
  • JpAllowDowngrades. Set to "yes" if --win-upgrade-uuid command line option + * was specified. Undefined otherwise + *
  • JpLicenseRtf. Set to the value of --license-file command line option. + * Undefined is --license-file command line option was not specified + *
  • JpInstallDirChooser. Set to "yes" if --win-dir-chooser command line + * option was specified. Undefined otherwise + *
  • JpConfigDir. Absolute path to the directory with generated WiX source + * files. + *
  • JpIsSystemWide. Set to "yes" if --win-per-user-install command line + * option was not specified. Undefined otherwise + *
+ */ +public class WinMsiBundler extends AbstractBundler { + + public static final BundlerParamInfo APP_BUNDLER = + new WindowsBundlerParam<>( + "win.app.bundler", + WinAppBundler.class, + params -> new WinAppBundler(), + null); + + public static final BundlerParamInfo MSI_IMAGE_DIR = + new WindowsBundlerParam<>( + "win.msi.imageDir", + File.class, + params -> { + File imagesRoot = IMAGES_ROOT.fetchFrom(params); + if (!imagesRoot.exists()) imagesRoot.mkdirs(); + return new File(imagesRoot, "win-msi.image"); + }, + (s, p) -> null); + + public static final BundlerParamInfo WIN_APP_IMAGE = + new WindowsBundlerParam<>( + "win.app.image", + File.class, + null, + (s, p) -> null); + + public static final StandardBundlerParam MSI_SYSTEM_WIDE = + new StandardBundlerParam<>( + Arguments.CLIOptions.WIN_PER_USER_INSTALLATION.getId(), + Boolean.class, + params -> true, // MSIs default to system wide + // valueOf(null) is false, + // and we actually do want null + (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null + : Boolean.valueOf(s) + ); + + + public static final StandardBundlerParam PRODUCT_VERSION = + new StandardBundlerParam<>( + "win.msi.productVersion", + String.class, + VERSION::fetchFrom, + (s, p) -> s + ); + + private static final BundlerParamInfo UPGRADE_UUID = + new WindowsBundlerParam<>( + Arguments.CLIOptions.WIN_UPGRADE_UUID.getId(), + String.class, + null, + (s, p) -> s); + + @Override + public String getName() { + return I18N.getString("msi.bundler.name"); + } + + @Override + public String getID() { + return "msi"; + } + + @Override + public String getBundleType() { + return "INSTALLER"; + } + + @Override + public File execute(Map params, + File outputParentDir) throws PackagerException { + return bundle(params, outputParentDir); + } + + @Override + public boolean supported(boolean platformInstaller) { + try { + if (wixToolset == null) { + wixToolset = WixTool.toolset(); + } + return true; + } catch (ConfigException ce) { + Log.error(ce.getMessage()); + if (ce.getAdvice() != null) { + Log.error(ce.getAdvice()); + } + } catch (Exception e) { + Log.error(e.getMessage()); + } + return false; + } + + @Override + public boolean isDefault() { + return false; + } + + private static UUID getUpgradeCode(Map params) { + String upgradeCode = UPGRADE_UUID.fetchFrom(params); + if (upgradeCode != null) { + return UUID.fromString(upgradeCode); + } + return createNameUUID("UpgradeCode", params, List.of(VENDOR, APP_NAME)); + } + + private static UUID getProductCode(Map params) { + return createNameUUID("ProductCode", params, List.of(VENDOR, APP_NAME, + VERSION)); + } + + private static UUID createNameUUID(String prefix, + Map params, + List> components) { + String key = Stream.concat(Stream.of(prefix), components.stream().map( + c -> c.fetchFrom(params))).collect(Collectors.joining("/")); + return UUID.nameUUIDFromBytes(key.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public boolean validate(Map params) + throws ConfigException { + try { + if (wixToolset == null) { + wixToolset = WixTool.toolset(); + } + + try { + getUpgradeCode(params); + } catch (IllegalArgumentException ex) { + throw new ConfigException(ex); + } + + for (var toolInfo: wixToolset.values()) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.tool-version"), toolInfo.path.getFileName(), + toolInfo.version)); + } + + wixSourcesBuilder.setWixVersion(wixToolset.get(WixTool.Light).version); + + wixSourcesBuilder.logWixFeatures(); + + /********* validate bundle parameters *************/ + + String version = PRODUCT_VERSION.fetchFrom(params); + if (!isVersionStringValid(version)) { + throw new ConfigException( + MessageFormat.format(I18N.getString( + "error.version-string-wrong-format"), version), + MessageFormat.format(I18N.getString( + "error.version-string-wrong-format.advice"), + PRODUCT_VERSION.getID())); + } + + // only one mime type per association, at least one file extension + List> associations = + FILE_ASSOCIATIONS.fetchFrom(params); + if (associations != null) { + for (int i = 0; i < associations.size(); i++) { + Map assoc = associations.get(i); + List mimes = FA_CONTENT_TYPE.fetchFrom(assoc); + if (mimes.size() > 1) { + throw new ConfigException(MessageFormat.format( + I18N.getString("error.too-many-content-types-for-file-association"), i), + I18N.getString("error.too-many-content-types-for-file-association.advice")); + } + } + } + + return true; + } catch (RuntimeException re) { + if (re.getCause() instanceof ConfigException) { + throw (ConfigException) re.getCause(); + } else { + throw new ConfigException(re); + } + } + } + + // https://msdn.microsoft.com/en-us/library/aa370859%28v=VS.85%29.aspx + // The format of the string is as follows: + // major.minor.build + // The first field is the major version and has a maximum value of 255. + // The second field is the minor version and has a maximum value of 255. + // The third field is called the build version or the update version and + // has a maximum value of 65,535. + static boolean isVersionStringValid(String v) { + if (v == null) { + return true; + } + + String p[] = v.split("\\."); + if (p.length > 3) { + Log.verbose(I18N.getString( + "message.version-string-too-many-components")); + return false; + } + + try { + int val = Integer.parseInt(p[0]); + if (val < 0 || val > 255) { + Log.verbose(I18N.getString( + "error.version-string-major-out-of-range")); + return false; + } + if (p.length > 1) { + val = Integer.parseInt(p[1]); + if (val < 0 || val > 255) { + Log.verbose(I18N.getString( + "error.version-string-minor-out-of-range")); + return false; + } + } + if (p.length > 2) { + val = Integer.parseInt(p[2]); + if (val < 0 || val > 65535) { + Log.verbose(I18N.getString( + "error.version-string-build-out-of-range")); + return false; + } + } + } catch (NumberFormatException ne) { + Log.verbose(I18N.getString("error.version-string-part-not-number")); + Log.verbose(ne); + return false; + } + + return true; + } + + private void prepareProto(Map params) + throws PackagerException, IOException { + File appImage = StandardBundlerParam.getPredefinedAppImage(params); + File appDir = null; + + // we either have an application image or need to build one + if (appImage != null) { + appDir = new File(MSI_IMAGE_DIR.fetchFrom(params), + APP_NAME.fetchFrom(params)); + // copy everything from appImage dir into appDir/name + IOUtils.copyRecursive(appImage.toPath(), appDir.toPath()); + } else { + appDir = APP_BUNDLER.fetchFrom(params).doBundle(params, + MSI_IMAGE_DIR.fetchFrom(params), true); + } + + params.put(WIN_APP_IMAGE.getID(), appDir); + + String licenseFile = LICENSE_FILE.fetchFrom(params); + if (licenseFile != null) { + // need to copy license file to the working directory + // and convert to rtf if needed + File lfile = new File(licenseFile); + File destFile = new File(CONFIG_ROOT.fetchFrom(params), + lfile.getName()); + + IOUtils.copyFile(lfile, destFile); + destFile.setWritable(true); + ensureByMutationFileIsRTF(destFile); + } + } + + public File bundle(Map params, File outdir) + throws PackagerException { + + IOUtils.writableOutputDir(outdir.toPath()); + + Path imageDir = MSI_IMAGE_DIR.fetchFrom(params).toPath(); + try { + Files.createDirectories(imageDir); + + prepareProto(params); + + wixSourcesBuilder + .initFromParams(WIN_APP_IMAGE.fetchFrom(params).toPath(), params) + .createMainFragment(CONFIG_ROOT.fetchFrom(params).toPath().resolve( + "bundle.wxf")); + + Map wixVars = prepareMainProjectFile(params); + + new ScriptRunner() + .setDirectory(imageDir) + .setResourceCategoryId("resource.post-app-image-script") + .setScriptNameSuffix("post-image") + .setEnvironmentVariable("JpAppImageDir", imageDir.toAbsolutePath().toString()) + .run(params); + + return buildMSI(params, wixVars, outdir); + } catch (IOException ex) { + Log.verbose(ex); + throw new PackagerException(ex); + } + } + + Map prepareMainProjectFile( + Map params) throws IOException { + Map data = new HashMap<>(); + + final UUID productCode = getProductCode(params); + final UUID upgradeCode = getUpgradeCode(params); + + data.put("JpProductCode", productCode.toString()); + data.put("JpProductUpgradeCode", upgradeCode.toString()); + + Log.verbose(MessageFormat.format(I18N.getString("message.product-code"), + productCode)); + Log.verbose(MessageFormat.format(I18N.getString("message.upgrade-code"), + upgradeCode)); + + data.put("JpAllowUpgrades", "yes"); + + data.put("JpAppName", APP_NAME.fetchFrom(params)); + data.put("JpAppDescription", DESCRIPTION.fetchFrom(params)); + data.put("JpAppVendor", VENDOR.fetchFrom(params)); + data.put("JpAppVersion", PRODUCT_VERSION.fetchFrom(params)); + + final Path configDir = CONFIG_ROOT.fetchFrom(params).toPath(); + + data.put("JpConfigDir", configDir.toAbsolutePath().toString()); + + if (MSI_SYSTEM_WIDE.fetchFrom(params)) { + data.put("JpIsSystemWide", "yes"); + } + + String licenseFile = LICENSE_FILE.fetchFrom(params); + if (licenseFile != null) { + String lname = new File(licenseFile).getName(); + File destFile = new File(CONFIG_ROOT.fetchFrom(params), lname); + data.put("JpLicenseRtf", destFile.getAbsolutePath()); + } + + // Copy CA dll to include with installer + if (INSTALLDIR_CHOOSER.fetchFrom(params)) { + data.put("JpInstallDirChooser", "yes"); + String fname = "wixhelper.dll"; + try (InputStream is = OverridableResource.readDefault(fname)) { + Files.copy(is, Paths.get( + CONFIG_ROOT.fetchFrom(params).getAbsolutePath(), + fname)); + } + } + + // Copy l10n files. + for (String loc : Arrays.asList("en", "ja", "zh_CN")) { + String fname = "MsiInstallerStrings_" + loc + ".wxl"; + try (InputStream is = OverridableResource.readDefault(fname)) { + Files.copy(is, Paths.get( + CONFIG_ROOT.fetchFrom(params).getAbsolutePath(), + fname)); + } + } + + createResource("main.wxs", params) + .setCategory(I18N.getString("resource.main-wix-file")) + .saveToFile(configDir.resolve("main.wxs")); + + createResource("overrides.wxi", params) + .setCategory(I18N.getString("resource.overrides-wix-file")) + .saveToFile(configDir.resolve("overrides.wxi")); + + return data; + } + + private File buildMSI(Map params, + Map wixVars, File outdir) + throws IOException { + + File msiOut = new File( + outdir, INSTALLER_FILE_NAME.fetchFrom(params) + ".msi"); + + Log.verbose(MessageFormat.format(I18N.getString( + "message.preparing-msi-config"), msiOut.getAbsolutePath())); + + WixPipeline wixPipeline = new WixPipeline() + .setToolset(wixToolset.entrySet().stream().collect( + Collectors.toMap( + entry -> entry.getKey(), + entry -> entry.getValue().path))) + .setWixObjDir(TEMP_ROOT.fetchFrom(params).toPath().resolve("wixobj")) + .setWorkDir(WIN_APP_IMAGE.fetchFrom(params).toPath()) + .addSource(CONFIG_ROOT.fetchFrom(params).toPath().resolve("main.wxs"), wixVars) + .addSource(CONFIG_ROOT.fetchFrom(params).toPath().resolve("bundle.wxf"), null); + + Log.verbose(MessageFormat.format(I18N.getString( + "message.generating-msi"), msiOut.getAbsolutePath())); + + boolean enableLicenseUI = (LICENSE_FILE.fetchFrom(params) != null); + boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params); + + List lightArgs = new ArrayList<>(); + + if (!MSI_SYSTEM_WIDE.fetchFrom(params)) { + wixPipeline.addLightOptions("-sice:ICE91"); + } + if (enableLicenseUI || enableInstalldirUI) { + wixPipeline.addLightOptions("-ext", "WixUIExtension"); + } + + wixPipeline.addLightOptions("-loc", + CONFIG_ROOT.fetchFrom(params).toPath().resolve(I18N.getString( + "resource.wxl-file-name")).toAbsolutePath().toString()); + + // Only needed if we using CA dll, so Wix can find it + if (enableInstalldirUI) { + wixPipeline.addLightOptions("-b", CONFIG_ROOT.fetchFrom(params).getAbsolutePath()); + } + + wixPipeline.buildMsi(msiOut.toPath().toAbsolutePath()); + + return msiOut; + } + + public static void ensureByMutationFileIsRTF(File f) { + if (f == null || !f.isFile()) return; + + try { + boolean existingLicenseIsRTF = false; + + try (FileInputStream fin = new FileInputStream(f)) { + byte[] firstBits = new byte[7]; + + if (fin.read(firstBits) == firstBits.length) { + String header = new String(firstBits); + existingLicenseIsRTF = "{\\rtf1\\".equals(header); + } + } + + if (!existingLicenseIsRTF) { + List oldLicense = Files.readAllLines(f.toPath()); + try (Writer w = Files.newBufferedWriter( + f.toPath(), Charset.forName("Windows-1252"))) { + w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033" + + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n" + + "\\viewkind4\\uc1\\pard\\sa200\\sl276" + + "\\slmult1\\lang9\\fs20 "); + oldLicense.forEach(l -> { + try { + for (char c : l.toCharArray()) { + // 0x00 <= ch < 0x20 Escaped (\'hh) + // 0x20 <= ch < 0x80 Raw(non - escaped) char + // 0x80 <= ch <= 0xFF Escaped(\ 'hh) + // 0x5C, 0x7B, 0x7D (special RTF characters + // \,{,})Escaped(\'hh) + // ch > 0xff Escaped (\\ud###?) + if (c < 0x10) { + w.write("\\'0"); + w.write(Integer.toHexString(c)); + } else if (c > 0xff) { + w.write("\\ud"); + w.write(Integer.toString(c)); + // \\uc1 is in the header and in effect + // so we trail with a replacement char if + // the font lacks that character - '?' + w.write("?"); + } else if ((c < 0x20) || (c >= 0x80) || + (c == 0x5C) || (c == 0x7B) || + (c == 0x7D)) { + w.write("\\'"); + w.write(Integer.toHexString(c)); + } else { + w.write(c); + } + } + // blank lines are interpreted as paragraph breaks + if (l.length() < 1) { + w.write("\\par"); + } else { + w.write(" "); + } + w.write("\r\n"); + } catch (IOException e) { + Log.verbose(e); + } + }); + w.write("}\r\n"); + } + } + } catch (IOException e) { + Log.verbose(e); + } + + } + + private Map wixToolset; + private WixSourcesBuilder wixSourcesBuilder = new WixSourcesBuilder(); + +} diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsAppImageBuilder.java b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsAppImageBuilder.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsAppImageBuilder.java @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import static jdk.incubator.jpackage.internal.OverridableResource.createResource; + +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; + +public class WindowsAppImageBuilder extends AbstractAppImageBuilder { + + static { + System.loadLibrary("jpackage"); + } + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.WinResources"); + + private final static String LIBRARY_NAME = "applauncher.dll"; + private final static String REDIST_MSVCR = "vcruntimeVS_VER.dll"; + private final static String REDIST_MSVCP = "msvcpVS_VER.dll"; + + private final static String TEMPLATE_APP_ICON ="java48.ico"; + + private static final String EXECUTABLE_PROPERTIES_TEMPLATE = + "WinLauncher.template"; + + private final Path root; + private final Path appDir; + private final Path appModsDir; + private final Path runtimeDir; + private final Path mdir; + private final Path binDir; + + public static final BundlerParamInfo REBRAND_EXECUTABLE = + new WindowsBundlerParam<>( + "win.launcher.rebrand", + Boolean.class, + params -> Boolean.TRUE, + (s, p) -> Boolean.valueOf(s)); + + public static final BundlerParamInfo ICON_ICO = + new StandardBundlerParam<>( + "icon.ico", + File.class, + params -> { + File f = ICON.fetchFrom(params); + if (f != null && !f.getName().toLowerCase().endsWith(".ico")) { + Log.error(MessageFormat.format( + I18N.getString("message.icon-not-ico"), f)); + return null; + } + return f; + }, + (s, p) -> new File(s)); + + public static final StandardBundlerParam CONSOLE_HINT = + new WindowsBundlerParam<>( + Arguments.CLIOptions.WIN_CONSOLE_HINT.getId(), + Boolean.class, + params -> false, + // valueOf(null) is false, + // and we actually do want null in some cases + (s, p) -> (s == null + || "null".equalsIgnoreCase(s)) ? true : Boolean.valueOf(s)); + + public WindowsAppImageBuilder(Map params, Path imageOutDir) + throws IOException { + super(params, + imageOutDir.resolve(APP_NAME.fetchFrom(params) + "/runtime")); + + Objects.requireNonNull(imageOutDir); + + this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params)); + this.appDir = root.resolve("app"); + this.appModsDir = appDir.resolve("mods"); + this.runtimeDir = root.resolve("runtime"); + this.mdir = runtimeDir.resolve("lib"); + this.binDir = root; + Files.createDirectories(appDir); + Files.createDirectories(runtimeDir); + } + + private void writeEntry(InputStream in, Path dstFile) throws IOException { + Files.createDirectories(dstFile.getParent()); + Files.copy(in, dstFile); + } + + private static String getLauncherName(Map params) { + return APP_NAME.fetchFrom(params) + ".exe"; + } + + // Returns launcher resource name for launcher we need to use. + public static String getLauncherResourceName( + Map params) { + if (CONSOLE_HINT.fetchFrom(params)) { + return "jpackageapplauncher.exe"; + } else { + return "jpackageapplauncherw.exe"; + } + } + + public static String getLauncherCfgName( + Map params) { + return "app/" + APP_NAME.fetchFrom(params) +".cfg"; + } + + private File getConfig_AppIcon(Map params) { + return new File(getConfigRoot(params), + APP_NAME.fetchFrom(params) + ".ico"); + } + + private File getConfig_ExecutableProperties( + Map params) { + return new File(getConfigRoot(params), + APP_NAME.fetchFrom(params) + ".properties"); + } + + File getConfigRoot(Map params) { + return CONFIG_ROOT.fetchFrom(params); + } + + @Override + public Path getAppDir() { + return appDir; + } + + @Override + public Path getAppModsDir() { + return appModsDir; + } + + @Override + public void prepareApplicationFiles(Map params) + throws IOException { + Map originalParams = new HashMap<>(params); + + try { + IOUtils.writableOutputDir(root); + IOUtils.writableOutputDir(binDir); + } catch (PackagerException pe) { + throw new RuntimeException(pe); + } + AppImageFile.save(root, params); + + // create the .exe launchers + createLauncherForEntryPoint(params); + + // copy the jars + copyApplication(params); + + // copy in the needed libraries + try (InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) { + Files.copy(is_lib, binDir.resolve(LIBRARY_NAME)); + } + + copyMSVCDLLs(); + + // create the additional launcher(s), if any + List> entryPoints = + StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params); + for (Map entryPoint : entryPoints) { + createLauncherForEntryPoint( + AddLauncherArguments.merge(originalParams, entryPoint)); + } + } + + @Override + public void prepareJreFiles(Map params) + throws IOException {} + + private void copyMSVCDLLs() throws IOException { + AtomicReference ioe = new AtomicReference<>(); + try (Stream files = Files.list(runtimeDir.resolve("bin"))) { + files.filter(p -> Pattern.matches( + "^(vcruntime|msvcp|msvcr|ucrtbase|api-ms-win-).*\\.dll$", + p.toFile().getName().toLowerCase())) + .forEach(p -> { + try { + Files.copy(p, binDir.resolve((p.toFile().getName()))); + } catch (IOException e) { + ioe.set(e); + } + }); + } + + IOException e = ioe.get(); + if (e != null) { + throw e; + } + } + + private void validateValueAndPut( + Map data, String key, + BundlerParamInfo param, + Map params) { + String value = param.fetchFrom(params); + if (value.contains("\r") || value.contains("\n")) { + Log.error("Configuration Parameter " + param.getID() + + " contains multiple lines of text, ignore it"); + data.put(key, ""); + return; + } + data.put(key, value); + } + + protected void prepareExecutableProperties( + Map params) throws IOException { + + Map data = new HashMap<>(); + + // mapping Java parameters in strings for version resource + validateValueAndPut(data, "COMPANY_NAME", VENDOR, params); + validateValueAndPut(data, "FILE_DESCRIPTION", DESCRIPTION, params); + validateValueAndPut(data, "FILE_VERSION", VERSION, params); + data.put("INTERNAL_NAME", getLauncherName(params)); + validateValueAndPut(data, "LEGAL_COPYRIGHT", COPYRIGHT, params); + data.put("ORIGINAL_FILENAME", getLauncherName(params)); + validateValueAndPut(data, "PRODUCT_NAME", APP_NAME, params); + validateValueAndPut(data, "PRODUCT_VERSION", VERSION, params); + + createResource(EXECUTABLE_PROPERTIES_TEMPLATE, params) + .setCategory(I18N.getString("resource.executable-properties-template")) + .setSubstitutionData(data) + .saveToFile(getConfig_ExecutableProperties(params)); + } + + private void createLauncherForEntryPoint( + Map params) throws IOException { + + File iconTarget = getConfig_AppIcon(params); + + createResource(TEMPLATE_APP_ICON, params) + .setCategory("icon") + .setExternal(ICON_ICO.fetchFrom(params)) + .saveToFile(iconTarget); + + writeCfgFile(params, root.resolve( + getLauncherCfgName(params)).toFile()); + + prepareExecutableProperties(params); + + // Copy executable to bin folder + Path executableFile = binDir.resolve(getLauncherName(params)); + + try (InputStream is_launcher = + getResourceAsStream(getLauncherResourceName(params))) { + writeEntry(is_launcher, executableFile); + } + + File launcher = executableFile.toFile(); + launcher.setWritable(true, true); + + // Update branding of EXE file + if (REBRAND_EXECUTABLE.fetchFrom(params)) { + try { + String tempDirectory = WindowsDefender.getUserTempDirectory(); + if (Arguments.CLIOptions.context().userProvidedBuildRoot) { + tempDirectory = + TEMP_ROOT.fetchFrom(params).getAbsolutePath(); + } + if (WindowsDefender.isThereAPotentialWindowsDefenderIssue( + tempDirectory)) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.potential.windows.defender.issue"), + tempDirectory)); + } + + launcher.setWritable(true); + + if (iconTarget.exists()) { + iconSwap(iconTarget.getAbsolutePath(), + launcher.getAbsolutePath()); + } + + File executableProperties = + getConfig_ExecutableProperties(params); + + if (executableProperties.exists()) { + if (versionSwap(executableProperties.getAbsolutePath(), + launcher.getAbsolutePath()) != 0) { + throw new RuntimeException(MessageFormat.format( + I18N.getString("error.version-swap"), + executableProperties.getAbsolutePath())); + } + } + } finally { + executableFile.toFile().setExecutable(true); + executableFile.toFile().setReadOnly(); + } + } + + Files.copy(iconTarget.toPath(), + binDir.resolve(APP_NAME.fetchFrom(params) + ".ico")); + } + + private void copyApplication(Map params) + throws IOException { + List appResourcesList = + APP_RESOURCES_LIST.fetchFrom(params); + if (appResourcesList == null) { + throw new RuntimeException("Null app resources?"); + } + for (RelativeFileSet appResources : appResourcesList) { + if (appResources == null) { + throw new RuntimeException("Null app resources?"); + } + File srcdir = appResources.getBaseDirectory(); + for (String fname : appResources.getIncludedFiles()) { + copyEntry(appDir, srcdir, fname); + } + } + } + + private static native int iconSwap(String iconTarget, String launcher); + + private static native int versionSwap(String executableProperties, + String launcher); + +} diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsBundlerParam.java b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsBundlerParam.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsBundlerParam.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.text.MessageFormat; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.function.BiFunction; +import java.util.function.Function; + +class WindowsBundlerParam extends StandardBundlerParam { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.incubator.jpackage.internal.resources.WinResources"); + + WindowsBundlerParam(String id, Class valueType, + Function, T> defaultValueFunction, + BiFunction, T> stringConverter) { + super(id, valueType, defaultValueFunction, stringConverter); + } + + static final BundlerParamInfo INSTALLER_FILE_NAME = + new StandardBundlerParam<> ( + "win.installerName", + String.class, + params -> { + String nm = APP_NAME.fetchFrom(params); + if (nm == null) return null; + + String version = VERSION.fetchFrom(params); + if (version == null) { + return nm; + } else { + return nm + "-" + version; + } + }, + (s, p) -> s); + + static final StandardBundlerParam MENU_GROUP = + new StandardBundlerParam<>( + Arguments.CLIOptions.WIN_MENU_GROUP.getId(), + String.class, + params -> I18N.getString("param.menu-group.default"), + (s, p) -> s + ); + + static final BundlerParamInfo INSTALLDIR_CHOOSER = + new StandardBundlerParam<> ( + Arguments.CLIOptions.WIN_DIR_CHOOSER.getId(), + Boolean.class, + params -> Boolean.FALSE, + (s, p) -> Boolean.valueOf(s) + ); + + static final BundlerParamInfo WINDOWS_INSTALL_DIR = + new StandardBundlerParam<>( + "windows-install-dir", + String.class, + params -> { + String dir = INSTALL_DIR.fetchFrom(params); + if (dir != null) { + if (dir.contains(":") || dir.contains("..")) { + Log.error(MessageFormat.format(I18N.getString( + "message.invalid.install.dir"), dir, + APP_NAME.fetchFrom(params))); + } else { + if (dir.startsWith("\\")) { + dir = dir.substring(1); + } + if (dir.endsWith("\\")) { + dir = dir.substring(0, dir.length() - 1); + } + return dir; + } + } + return APP_NAME.fetchFrom(params); // Default to app name + }, + (s, p) -> s + ); +} diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsDefender.java b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsDefender.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsDefender.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.util.List; + +final class WindowsDefender { + + private WindowsDefender() {} + + static final boolean isThereAPotentialWindowsDefenderIssue(String dir) { + boolean result = false; + + if (Platform.getPlatform() == Platform.WINDOWS && + Platform.getMajorVersion() == 10) { + + // If DisableRealtimeMonitoring is not enabled then there + // may be a problem. + if (!WindowsRegistry.readDisableRealtimeMonitoring() && + !isDirectoryInExclusionPath(dir)) { + result = true; + } + } + + return result; + } + + private static boolean isDirectoryInExclusionPath(String dir) { + boolean result = false; + // If the user temp directory is not found in the exclusion + // list then there may be a problem. + List paths = WindowsRegistry.readExclusionsPaths(); + for (String s : paths) { + if (WindowsRegistry.comparePaths(s, dir)) { + result = true; + break; + } + } + + return result; + } + + static final String getUserTempDirectory() { + String tempDirectory = System.getProperty("java.io.tmpdir"); + return tempDirectory; + } +} diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsRegistry.java b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsRegistry.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsRegistry.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +final class WindowsRegistry { + + // Currently we only support HKEY_LOCAL_MACHINE. Native implementation will + // require support for additinal HKEY if needed. + private static final int HKEY_LOCAL_MACHINE = 1; + + static { + System.loadLibrary("jpackage"); + } + + private WindowsRegistry() {} + + /** + * Reads the registry value for DisableRealtimeMonitoring. + * @return true if DisableRealtimeMonitoring is set to 0x1, + * false otherwise. + */ + static final boolean readDisableRealtimeMonitoring() { + final String subKey = "Software\\Microsoft\\" + + "Windows Defender\\Real-Time Protection"; + final String value = "DisableRealtimeMonitoring"; + int result = readDwordValue(HKEY_LOCAL_MACHINE, subKey, value, 0); + return (result == 1); + } + + static final List readExclusionsPaths() { + List result = new ArrayList<>(); + final String subKey = "Software\\Microsoft\\" + + "Windows Defender\\Exclusions\\Paths"; + long lKey = openRegistryKey(HKEY_LOCAL_MACHINE, subKey); + if (lKey == 0) { + return result; + } + + String valueName; + int index = 0; + do { + valueName = enumRegistryValue(lKey, index); + if (valueName != null) { + result.add(valueName); + index++; + } + } while (valueName != null); + + closeRegistryKey(lKey); + + return result; + } + + /** + * Reads DWORD registry value. + * + * @param key one of HKEY predefine value + * @param subKey registry sub key + * @param value value to read + * @param defaultValue default value in case if subKey or value not found + * or any other errors occurred + * @return value's data only if it was read successfully, otherwise + * defaultValue + */ + private static native int readDwordValue(int key, String subKey, + String value, int defaultValue); + + /** + * Open registry key. + * + * @param key one of HKEY predefine value + * @param subKey registry sub key + * @return native handle to open key + */ + private static native long openRegistryKey(int key, String subKey); + + /** + * Enumerates the values for registry key. + * + * @param lKey native handle to open key returned by openRegistryKey + * @param index index of value starting from 0. Increment until this + * function returns NULL which means no more values. + * @return returns value or NULL if error or no more data + */ + private static native String enumRegistryValue(long lKey, int index); + + /** + * Close registry key. + * + * @param lKey native handle to open key returned by openRegistryKey + */ + private static native void closeRegistryKey(long lKey); + + /** + * Compares two Windows paths regardless case and if paths + * are short or long. + * + * @param path1 path to compare + * @param path2 path to compare + * @return true if paths point to same location + */ + public static native boolean comparePaths(String path1, String path2); +} diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixPipeline.java b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixPipeline.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixPipeline.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +/** + * WiX pipeline. Compiles and links WiX sources. + */ +public class WixPipeline { + WixPipeline() { + sources = new ArrayList<>(); + lightOptions = new ArrayList<>(); + } + + WixPipeline setToolset(Map v) { + toolset = v; + return this; + } + + WixPipeline setWixVariables(Map v) { + wixVariables = v; + return this; + } + + WixPipeline setWixObjDir(Path v) { + wixObjDir = v; + return this; + } + + WixPipeline setWorkDir(Path v) { + workDir = v; + return this; + } + + WixPipeline addSource(Path source, Map wixVariables) { + WixSource entry = new WixSource(); + entry.source = source; + entry.variables = wixVariables; + sources.add(entry); + return this; + } + + WixPipeline addLightOptions(String ... v) { + lightOptions.addAll(List.of(v)); + return this; + } + + void buildMsi(Path msi) throws IOException { + List wixObjs = new ArrayList<>(); + for (var source : sources) { + wixObjs.add(compile(source)); + } + + List lightCmdline = new ArrayList<>(List.of( + toolset.get(WixTool.Light).toString(), + "-nologo", + "-spdb", + "-ext", "WixUtilExtension", + "-out", msi.toString() + )); + + lightCmdline.addAll(lightOptions); + wixObjs.stream().map(Path::toString).forEach(lightCmdline::add); + + Files.createDirectories(msi.getParent()); + execute(lightCmdline); + } + + private Path compile(WixSource wixSource) throws IOException { + UnaryOperator adjustPath = path -> { + return workDir != null ? path.toAbsolutePath() : path; + }; + + Path wixObj = adjustPath.apply(wixObjDir).resolve(IOUtils.replaceSuffix( + wixSource.source.getFileName(), ".wixobj")); + + List cmdline = new ArrayList<>(List.of( + toolset.get(WixTool.Candle).toString(), + "-nologo", + adjustPath.apply(wixSource.source).toString(), + "-ext", "WixUtilExtension", + "-arch", "x64", + "-out", wixObj.toAbsolutePath().toString() + )); + + Map appliedVaribales = new HashMap<>(); + Stream.of(wixVariables, wixSource.variables) + .filter(Objects::nonNull) + .forEachOrdered(appliedVaribales::putAll); + + appliedVaribales.entrySet().stream().map(wixVar -> String.format("-d%s=%s", + wixVar.getKey(), wixVar.getValue())).forEachOrdered( + cmdline::add); + + execute(cmdline); + + return wixObj; + } + + private void execute(List cmdline) throws IOException { + Executor.of(new ProcessBuilder(cmdline).directory( + workDir != null ? workDir.toFile() : null)).executeExpectSuccess(); + } + + private final static class WixSource { + Path source; + Map variables; + } + + private Map toolset; + private Map wixVariables; + private List lightOptions; + private Path wixObjDir; + private Path workDir; + private List sources; +} diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixSourcesBuilder.java b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixSourcesBuilder.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixSourcesBuilder.java @@ -0,0 +1,847 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.*; +import java.util.function.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import jdk.incubator.jpackage.internal.IOUtils.XmlConsumer; +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; +import static jdk.incubator.jpackage.internal.WinMsiBundler.*; +import static jdk.incubator.jpackage.internal.WindowsBundlerParam.MENU_GROUP; +import static jdk.incubator.jpackage.internal.WindowsBundlerParam.WINDOWS_INSTALL_DIR; + +/** + * Creates application WiX source files. + */ +class WixSourcesBuilder { + + WixSourcesBuilder setWixVersion(DottedVersion v) { + wixVersion = v; + return this; + } + + WixSourcesBuilder initFromParams(Path appImageRoot, + Map params) { + Supplier appImageSupplier = () -> { + if (StandardBundlerParam.isRuntimeInstaller(params)) { + return ApplicationLayout.javaRuntime(); + } else { + return ApplicationLayout.platformAppImage(); + } + }; + + systemWide = MSI_SYSTEM_WIDE.fetchFrom(params); + + registryKeyPath = Path.of("Software", + VENDOR.fetchFrom(params), + APP_NAME.fetchFrom(params), + VERSION.fetchFrom(params)).toString(); + + installDir = (systemWide ? PROGRAM_FILES : LOCAL_PROGRAM_FILES).resolve( + WINDOWS_INSTALL_DIR.fetchFrom(params)); + + do { + ApplicationLayout layout = appImageSupplier.get(); + // Don't want AppImageFile.FILENAME in installed application. + // Register it with app image at a role without a match in installed + // app layout to exclude it from layout transformation. + layout.pathGroup().setPath(new Object(), + AppImageFile.getPathInAppImage(Path.of(""))); + + // Want absolute paths to source files in generated WiX sources. + // This is to handle scenario if sources would be processed from + // differnt current directory. + appImage = layout.resolveAt(appImageRoot.toAbsolutePath().normalize()); + } while (false); + + installedAppImage = appImageSupplier.get().resolveAt(INSTALLDIR); + + shortcutFolders = new HashSet<>(); + if (SHORTCUT_HINT.fetchFrom(params)) { + shortcutFolders.add(ShortcutsFolder.Desktop); + } + if (MENU_HINT.fetchFrom(params)) { + shortcutFolders.add(ShortcutsFolder.ProgramMenu); + } + + if (StandardBundlerParam.isRuntimeInstaller(params)) { + launcherPaths = Collections.emptyList(); + } else { + launcherPaths = AppImageFile.getLauncherNames(appImageRoot, params).stream() + .map(name -> installedAppImage.launchersDirectory().resolve(name)) + .map(WixSourcesBuilder::addExeSuffixToPath) + .collect(Collectors.toList()); + } + + programMenuFolderName = MENU_GROUP.fetchFrom(params); + + initFileAssociations(params); + + return this; + } + + void createMainFragment(Path file) throws IOException { + removeFolderItems = new HashMap<>(); + defaultedMimes = new HashSet<>(); + IOUtils.createXml(file, xml -> { + xml.writeStartElement("Wix"); + xml.writeDefaultNamespace("http://schemas.microsoft.com/wix/2006/wi"); + xml.writeNamespace("util", + "http://schemas.microsoft.com/wix/UtilExtension"); + + xml.writeStartElement("Fragment"); + + addFaComponentGroup(xml); + + addShortcutComponentGroup(xml); + + addFilesComponentGroup(xml); + + xml.writeEndElement(); // + + addIconsFragment(xml); + + xml.writeEndElement(); // + }); + } + + void logWixFeatures() { + if (wixVersion.compareTo("3.6") >= 0) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.use-wix36-features"), wixVersion)); + } + } + + private void normalizeFileAssociation(FileAssociation fa) { + fa.launcherPath = addExeSuffixToPath( + installedAppImage.launchersDirectory().resolve(fa.launcherPath)); + + if (fa.iconPath != null && !fa.iconPath.toFile().exists()) { + fa.iconPath = null; + } + + if (fa.iconPath != null) { + fa.iconPath = fa.iconPath.toAbsolutePath(); + } + + // Filter out empty extensions. + fa.extensions = fa.extensions.stream().filter(Predicate.not( + String::isEmpty)).collect(Collectors.toList()); + } + + private static Path addExeSuffixToPath(Path path) { + return IOUtils.addSuffix(path, ".exe"); + } + + private Path getInstalledFaIcoPath(FileAssociation fa) { + String fname = String.format("fa_%s.ico", String.join("_", fa.extensions)); + return installedAppImage.destktopIntegrationDirectory().resolve(fname); + } + + private void initFileAssociations(Map params) { + associations = FileAssociation.fetchFrom(params).stream() + .peek(this::normalizeFileAssociation) + // Filter out file associations without extensions. + .filter(fa -> !fa.extensions.isEmpty()) + .collect(Collectors.toList()); + + associations.stream().filter(fa -> fa.iconPath != null).forEach(fa -> { + // Need to add fa icon in the image. + Object key = new Object(); + appImage.pathGroup().setPath(key, fa.iconPath); + installedAppImage.pathGroup().setPath(key, getInstalledFaIcoPath(fa)); + }); + } + + private static UUID createNameUUID(String str) { + return UUID.nameUUIDFromBytes(str.getBytes(StandardCharsets.UTF_8)); + } + + private static UUID createNameUUID(Path path, String role) { + if (path.isAbsolute() || !ROOT_DIRS.contains(path.getName(0))) { + throw throwInvalidPathException(path); + } + // Paths are case insensitive on Windows + String keyPath = path.toString().toLowerCase(); + if (role != null) { + keyPath = role + "@" + keyPath; + } + return createNameUUID(keyPath); + } + + /** + * Value for Id attribute of various WiX elements. + */ + enum Id { + File, + Folder("dir"), + Shortcut, + ProgId, + Icon, + CreateFolder("mkdir"), + RemoveFolder("rm"); + + Id() { + this.prefix = name().toLowerCase(); + } + + Id(String prefix) { + this.prefix = prefix; + } + + String of(Path path) { + if (this == Folder && KNOWN_DIRS.contains(path)) { + return path.getFileName().toString(); + } + + String result = of(path, prefix, name()); + + if (this == Icon) { + // Icon id constructed from UUID value is too long and triggers + // CNDL1000 warning, so use Java hash code instead. + result = String.format("%s%d", prefix, result.hashCode()).replace( + "-", "_"); + } + + return result; + } + + private static String of(Path path, String prefix, String role) { + Objects.requireNonNull(role); + Objects.requireNonNull(prefix); + return String.format("%s%s", prefix, + createNameUUID(path, role).toString().replace("-", "")); + } + + static String of(Path path, String prefix) { + return of(path, prefix, prefix); + } + + private final String prefix; + } + + enum Component { + File(cfg().file()), + Shortcut(cfg().file().withRegistryKeyPath()), + ProgId(cfg().file().withRegistryKeyPath()), + CreateFolder(cfg().withRegistryKeyPath()), + RemoveFolder(cfg().withRegistryKeyPath()); + + Component() { + this.cfg = cfg(); + this.id = Id.valueOf(name()); + } + + Component(Config cfg) { + this.cfg = cfg; + this.id = Id.valueOf(name()); + } + + UUID guidOf(Path path) { + return createNameUUID(path, name()); + } + + String idOf(Path path) { + return id.of(path); + } + + boolean isRegistryKeyPath() { + return cfg.withRegistryKeyPath; + } + + boolean isFile() { + return cfg.isFile; + } + + static void startElement(XMLStreamWriter xml, String componentId, + String componentGuid) throws XMLStreamException, IOException { + xml.writeStartElement("Component"); + xml.writeAttribute("Win64", "yes"); + xml.writeAttribute("Id", componentId); + xml.writeAttribute("Guid", componentGuid); + } + + private static final class Config { + Config withRegistryKeyPath() { + withRegistryKeyPath = true; + return this; + } + + Config file() { + isFile = true; + return this; + } + + private boolean isFile; + private boolean withRegistryKeyPath; + } + + private static Config cfg() { + return new Config(); + } + + private final Config cfg; + private final Id id; + }; + + private static void addComponentGroup(XMLStreamWriter xml, String id, + List componentIds) throws XMLStreamException, IOException { + xml.writeStartElement("ComponentGroup"); + xml.writeAttribute("Id", id); + componentIds = componentIds.stream().filter(Objects::nonNull).collect( + Collectors.toList()); + for (var componentId : componentIds) { + xml.writeStartElement("ComponentRef"); + xml.writeAttribute("Id", componentId); + xml.writeEndElement(); + } + xml.writeEndElement(); + } + + private String addComponent(XMLStreamWriter xml, Path path, + Component role, XmlConsumer xmlConsumer) throws XMLStreamException, + IOException { + + final Path directoryRefPath; + if (role.isFile()) { + directoryRefPath = path.getParent(); + } else { + directoryRefPath = path; + } + + xml.writeStartElement("DirectoryRef"); + xml.writeAttribute("Id", Id.Folder.of(directoryRefPath)); + + final String componentId = "c" + role.idOf(path); + Component.startElement(xml, componentId, String.format("{%s}", + role.guidOf(path))); + + boolean isRegistryKeyPath = !systemWide || role.isRegistryKeyPath(); + if (isRegistryKeyPath) { + addRegistryKeyPath(xml, directoryRefPath); + if ((role.isFile() || (role == Component.CreateFolder + && !systemWide)) && !SYSTEM_DIRS.contains(directoryRefPath)) { + xml.writeStartElement("RemoveFolder"); + int counter = Optional.ofNullable(removeFolderItems.get( + directoryRefPath)).orElse(Integer.valueOf(0)).intValue() + 1; + removeFolderItems.put(directoryRefPath, counter); + xml.writeAttribute("Id", String.format("%s_%d", Id.RemoveFolder.of( + directoryRefPath), counter)); + xml.writeAttribute("On", "uninstall"); + xml.writeEndElement(); + } + } + + xml.writeStartElement(role.name()); + if (role != Component.CreateFolder) { + xml.writeAttribute("Id", role.idOf(path)); + } + + if (!isRegistryKeyPath) { + xml.writeAttribute("KeyPath", "yes"); + } + + xmlConsumer.accept(xml); + xml.writeEndElement(); + + xml.writeEndElement(); // + xml.writeEndElement(); // + + return componentId; + } + + private void addFaComponentGroup(XMLStreamWriter xml) + throws XMLStreamException, IOException { + + List componentIds = new ArrayList<>(); + for (var fa : associations) { + componentIds.addAll(addFaComponents(xml, fa)); + } + addComponentGroup(xml, "FileAssociations", componentIds); + } + + private void addShortcutComponentGroup(XMLStreamWriter xml) throws + XMLStreamException, IOException { + List componentIds = new ArrayList<>(); + Set defineShortcutFolders = new HashSet<>(); + for (var launcherPath : launcherPaths) { + for (var folder : shortcutFolders) { + String componentId = addShortcutComponent(xml, launcherPath, + folder); + if (componentId != null) { + defineShortcutFolders.add(folder); + componentIds.add(componentId); + } + } + } + + for (var folder : defineShortcutFolders) { + Path path = folder.getPath(this); + componentIds.addAll(addRootBranch(xml, path)); + } + + addComponentGroup(xml, "Shortcuts", componentIds); + } + + private String addShortcutComponent(XMLStreamWriter xml, Path launcherPath, + ShortcutsFolder folder) throws XMLStreamException, IOException { + Objects.requireNonNull(folder); + + if (!INSTALLDIR.equals(launcherPath.getName(0))) { + throw throwInvalidPathException(launcherPath); + } + + String launcherBasename = IOUtils.replaceSuffix( + launcherPath.getFileName(), "").toString(); + + Path shortcutPath = folder.getPath(this).resolve(launcherBasename); + return addComponent(xml, shortcutPath, Component.Shortcut, unused -> { + final Path icoFile = IOUtils.addSuffix( + installedAppImage.destktopIntegrationDirectory().resolve( + launcherBasename), ".ico"); + + xml.writeAttribute("Name", launcherBasename); + xml.writeAttribute("WorkingDirectory", INSTALLDIR.toString()); + xml.writeAttribute("Advertise", "no"); + xml.writeAttribute("IconIndex", "0"); + xml.writeAttribute("Target", String.format("[#%s]", + Component.File.idOf(launcherPath))); + xml.writeAttribute("Icon", Id.Icon.of(icoFile)); + }); + } + + private List addFaComponents(XMLStreamWriter xml, + FileAssociation fa) throws XMLStreamException, IOException { + List components = new ArrayList<>(); + for (var extension: fa.extensions) { + Path path = INSTALLDIR.resolve(String.format("%s_%s", extension, + fa.launcherPath.getFileName())); + components.add(addComponent(xml, path, Component.ProgId, unused -> { + xml.writeAttribute("Description", fa.description); + + if (fa.iconPath != null) { + xml.writeAttribute("Icon", Id.File.of(getInstalledFaIcoPath( + fa))); + xml.writeAttribute("IconIndex", "0"); + } + + xml.writeStartElement("Extension"); + xml.writeAttribute("Id", extension); + xml.writeAttribute("Advertise", "no"); + + var mimeIt = fa.mimeTypes.iterator(); + if (mimeIt.hasNext()) { + String mime = mimeIt.next(); + xml.writeAttribute("ContentType", mime); + + if (!defaultedMimes.contains(mime)) { + xml.writeStartElement("MIME"); + xml.writeAttribute("ContentType", mime); + xml.writeAttribute("Default", "yes"); + xml.writeEndElement(); + defaultedMimes.add(mime); + } + } + + xml.writeStartElement("Verb"); + xml.writeAttribute("Id", "open"); + xml.writeAttribute("Command", "Open"); + xml.writeAttribute("Argument", "%1"); + xml.writeAttribute("TargetFile", Id.File.of(fa.launcherPath)); + xml.writeEndElement(); // + + xml.writeEndElement(); // + })); + } + + return components; + } + + private List addRootBranch(XMLStreamWriter xml, Path path) + throws XMLStreamException, IOException { + if (!ROOT_DIRS.contains(path.getName(0))) { + throw throwInvalidPathException(path); + } + + Function createDirectoryName = dir -> null; + + boolean sysDir = true; + int levels = 1; + var dirIt = path.iterator(); + xml.writeStartElement("DirectoryRef"); + xml.writeAttribute("Id", dirIt.next().toString()); + + path = path.getName(0); + while (dirIt.hasNext()) { + levels++; + Path name = dirIt.next(); + path = path.resolve(name); + + if (sysDir && !SYSTEM_DIRS.contains(path)) { + sysDir = false; + createDirectoryName = dir -> dir.getFileName().toString(); + } + + final String directoryId; + if (!sysDir && path.equals(installDir)) { + directoryId = INSTALLDIR.toString(); + } else { + directoryId = Id.Folder.of(path); + } + xml.writeStartElement("Directory"); + xml.writeAttribute("Id", directoryId); + + String directoryName = createDirectoryName.apply(path); + if (directoryName != null) { + xml.writeAttribute("Name", directoryName); + } + } + + while (0 != levels--) { + xml.writeEndElement(); + } + + List componentIds = new ArrayList<>(); + while (!SYSTEM_DIRS.contains(path = path.getParent())) { + componentIds.add(addRemoveDirectoryComponent(xml, path)); + } + + return componentIds; + } + + private String addRemoveDirectoryComponent(XMLStreamWriter xml, Path path) + throws XMLStreamException, IOException { + return addComponent(xml, path, Component.RemoveFolder, + unused -> xml.writeAttribute("On", "uninstall")); + } + + private List addDirectoryHierarchy(XMLStreamWriter xml) + throws XMLStreamException, IOException { + + Set allDirs = new HashSet<>(); + Set emptyDirs = new HashSet<>(); + appImage.transform(installedAppImage, new PathGroup.TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + Path dir = dst.getParent(); + createDirectory(dir); + emptyDirs.remove(dir); + } + + @Override + public void createDirectory(final Path dir) throws IOException { + if (!allDirs.contains(dir)) { + emptyDirs.add(dir); + } + + Path it = dir; + while (it != null && allDirs.add(it)) { + it = it.getParent(); + } + + it = dir; + while ((it = it.getParent()) != null && emptyDirs.remove(it)); + } + }); + + List componentIds = new ArrayList<>(); + for (var dir : emptyDirs) { + componentIds.add(addComponent(xml, dir, Component.CreateFolder, + unused -> {})); + } + + if (!systemWide) { + // Per-user install requires component in every + // directory. + for (var dir : allDirs.stream() + .filter(Predicate.not(emptyDirs::contains)) + .filter(Predicate.not(removeFolderItems::containsKey)) + .collect(Collectors.toList())) { + componentIds.add(addRemoveDirectoryComponent(xml, dir)); + } + } + + allDirs.remove(INSTALLDIR); + for (var dir : allDirs) { + xml.writeStartElement("DirectoryRef"); + xml.writeAttribute("Id", Id.Folder.of(dir.getParent())); + xml.writeStartElement("Directory"); + xml.writeAttribute("Id", Id.Folder.of(dir)); + xml.writeAttribute("Name", dir.getFileName().toString()); + xml.writeEndElement(); + xml.writeEndElement(); + } + + componentIds.addAll(addRootBranch(xml, installDir)); + + return componentIds; + } + + private void addFilesComponentGroup(XMLStreamWriter xml) + throws XMLStreamException, IOException { + + List> files = new ArrayList<>(); + appImage.transform(installedAppImage, new PathGroup.TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + files.add(Map.entry(src, dst)); + } + + @Override + public void createDirectory(final Path dir) throws IOException { + } + }); + + List componentIds = new ArrayList<>(); + for (var file : files) { + Path src = file.getKey(); + Path dst = file.getValue(); + + componentIds.add(addComponent(xml, dst, Component.File, unused -> { + xml.writeAttribute("Source", src.normalize().toString()); + Path name = dst.getFileName(); + if (!name.equals(src.getFileName())) { + xml.writeAttribute("Name", name.toString()); + } + })); + } + + componentIds.addAll(addDirectoryHierarchy(xml)); + + componentIds.add(addDirectoryCleaner(xml, INSTALLDIR)); + + addComponentGroup(xml, "Files", componentIds); + } + + private void addIconsFragment(XMLStreamWriter xml) throws + XMLStreamException, IOException { + + PathGroup srcPathGroup = appImage.pathGroup(); + PathGroup dstPathGroup = installedAppImage.pathGroup(); + + // Build list of copy operations for all .ico files in application image + List> icoFiles = new ArrayList<>(); + srcPathGroup.transform(dstPathGroup, new PathGroup.TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + if (src.getFileName().toString().endsWith(".ico")) { + icoFiles.add(Map.entry(src, dst)); + } + } + + @Override + public void createDirectory(Path dst) throws IOException { + } + }); + + xml.writeStartElement("Fragment"); + for (var icoFile : icoFiles) { + xml.writeStartElement("Icon"); + xml.writeAttribute("Id", Id.Icon.of(icoFile.getValue())); + xml.writeAttribute("SourceFile", icoFile.getKey().toString()); + xml.writeEndElement(); + } + xml.writeEndElement(); + } + + private void addRegistryKeyPath(XMLStreamWriter xml, Path path) throws + XMLStreamException, IOException { + addRegistryKeyPath(xml, path, () -> "ProductCode", () -> "[ProductCode]"); + } + + private void addRegistryKeyPath(XMLStreamWriter xml, Path path, + Supplier nameAttr, Supplier valueAttr) throws + XMLStreamException, IOException { + + String regRoot = USER_PROFILE_DIRS.stream().anyMatch(path::startsWith) + || !systemWide ? "HKCU" : "HKLM"; + + xml.writeStartElement("RegistryKey"); + xml.writeAttribute("Root", regRoot); + xml.writeAttribute("Key", registryKeyPath); + if (wixVersion.compareTo("3.6") < 0) { + xml.writeAttribute("Action", "createAndRemoveOnUninstall"); + } + xml.writeStartElement("RegistryValue"); + xml.writeAttribute("Type", "string"); + xml.writeAttribute("KeyPath", "yes"); + xml.writeAttribute("Name", nameAttr.get()); + xml.writeAttribute("Value", valueAttr.get()); + xml.writeEndElement(); // + xml.writeEndElement(); // + } + + private String addDirectoryCleaner(XMLStreamWriter xml, Path path) throws + XMLStreamException, IOException { + if (wixVersion.compareTo("3.6") < 0) { + return null; + } + + // rm -rf + final String baseId = Id.of(path, "rm_rf"); + final String propertyId = baseId.toUpperCase(); + final String componentId = ("c" + baseId); + + xml.writeStartElement("Property"); + xml.writeAttribute("Id", propertyId); + xml.writeStartElement("RegistrySearch"); + xml.writeAttribute("Id", Id.of(path, "regsearch")); + xml.writeAttribute("Root", systemWide ? "HKLM" : "HKCU"); + xml.writeAttribute("Key", registryKeyPath); + xml.writeAttribute("Type", "raw"); + xml.writeAttribute("Name", propertyId); + xml.writeEndElement(); // + xml.writeEndElement(); // + + xml.writeStartElement("DirectoryRef"); + xml.writeAttribute("Id", INSTALLDIR.toString()); + Component.startElement(xml, componentId, "*"); + + addRegistryKeyPath(xml, INSTALLDIR, () -> propertyId, () -> { + // The following code converts a path to value to be saved in registry. + // E.g.: + // INSTALLDIR -> [INSTALLDIR] + // TERGETDIR/ProgramFiles64Folder/foo/bar -> [ProgramFiles64Folder]foo/bar + final Path rootDir = KNOWN_DIRS.stream() + .sorted(Comparator.comparing(Path::getNameCount).reversed()) + .filter(path::startsWith) + .findFirst().get(); + StringBuilder sb = new StringBuilder(); + sb.append(String.format("[%s]", rootDir.getFileName().toString())); + sb.append(rootDir.relativize(path).toString()); + return sb.toString(); + }); + + xml.writeStartElement( + "http://schemas.microsoft.com/wix/UtilExtension", + "RemoveFolderEx"); + xml.writeAttribute("On", "uninstall"); + xml.writeAttribute("Property", propertyId); + xml.writeEndElement(); // + xml.writeEndElement(); // + xml.writeEndElement(); // + + return componentId; + } + + private static IllegalArgumentException throwInvalidPathException(Path v) { + throw new IllegalArgumentException(String.format("Invalid path [%s]", v)); + } + + enum ShortcutsFolder { + ProgramMenu(PROGRAM_MENU_PATH), + Desktop(DESKTOP_PATH); + + private ShortcutsFolder(Path root) { + this.root = root; + } + + Path getPath(WixSourcesBuilder outer) { + if (this == ProgramMenu) { + return root.resolve(outer.programMenuFolderName); + } + return root; + } + + private final Path root; + } + + private DottedVersion wixVersion; + + private boolean systemWide; + + private String registryKeyPath; + + private Path installDir; + + private String programMenuFolderName; + + private List associations; + + private Set shortcutFolders; + + private List launcherPaths; + + private ApplicationLayout appImage; + private ApplicationLayout installedAppImage; + + private Map removeFolderItems; + private Set defaultedMimes; + + private final static Path TARGETDIR = Path.of("TARGETDIR"); + + private final static Path INSTALLDIR = Path.of("INSTALLDIR"); + + private final static Set ROOT_DIRS = Set.of(INSTALLDIR, TARGETDIR); + + private final static Path PROGRAM_MENU_PATH = TARGETDIR.resolve("ProgramMenuFolder"); + + private final static Path DESKTOP_PATH = TARGETDIR.resolve("DesktopFolder"); + + private final static Path PROGRAM_FILES = TARGETDIR.resolve("ProgramFiles64Folder"); + + private final static Path LOCAL_PROGRAM_FILES = TARGETDIR.resolve("LocalAppDataFolder"); + + private final static Set SYSTEM_DIRS = Set.of(TARGETDIR, + PROGRAM_MENU_PATH, DESKTOP_PATH, PROGRAM_FILES, LOCAL_PROGRAM_FILES); + + private final static Set KNOWN_DIRS = Stream.of(Set.of(INSTALLDIR), + SYSTEM_DIRS).flatMap(Set::stream).collect( + Collectors.toUnmodifiableSet()); + + private final static Set USER_PROFILE_DIRS = Set.of(LOCAL_PROGRAM_FILES, + PROGRAM_MENU_PATH, DESKTOP_PATH); + + private static final StandardBundlerParam MENU_HINT = + new WindowsBundlerParam<>( + Arguments.CLIOptions.WIN_MENU_HINT.getId(), + Boolean.class, + params -> false, + // valueOf(null) is false, + // and we actually do want null in some cases + (s, p) -> (s == null || + "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s) + ); + + private static final StandardBundlerParam SHORTCUT_HINT = + new WindowsBundlerParam<>( + Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(), + Boolean.class, + params -> false, + // valueOf(null) is false, + // and we actually do want null in some cases + (s, p) -> (s == null || + "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s) + ); +} diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixTool.java b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixTool.java new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixTool.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.incubator.jpackage.internal; + +import java.io.IOException; +import java.nio.file.*; +import java.text.MessageFormat; +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * WiX tool. + */ +public enum WixTool { + Candle, Light; + + static final class ToolInfo { + ToolInfo(Path path, String version) { + this.path = path; + this.version = new DottedVersion(version); + } + + final Path path; + final DottedVersion version; + } + + static Map toolset() throws ConfigException { + Map toolset = new HashMap<>(); + for (var tool : values()) { + toolset.put(tool, tool.find()); + } + return toolset; + } + + ToolInfo find() throws ConfigException { + final Path toolFileName = IOUtils.addSuffix( + Path.of(name().toLowerCase()), ".exe"); + + String[] version = new String[1]; + ConfigException reason = createToolValidator(toolFileName, version).get(); + if (version[0] != null) { + if (reason == null) { + // Found in PATH. + return new ToolInfo(toolFileName, version[0]); + } + + // Found in PATH, but something went wrong. + throw reason; + } + + for (var dir : findWixInstallDirs()) { + Path path = dir.resolve(toolFileName); + if (path.toFile().exists()) { + reason = createToolValidator(path, version).get(); + if (reason != null) { + throw reason; + } + return new ToolInfo(path, version[0]); + } + } + + throw reason; + } + + private static Supplier createToolValidator(Path toolPath, + String[] versionCtnr) { + return new ToolValidator(toolPath) + .setCommandLine("/?") + .setMinimalVersion(MINIMAL_VERSION) + .setToolNotFoundErrorHandler( + (name, ex) -> new ConfigException( + I18N.getString("error.no-wix-tools"), + I18N.getString("error.no-wix-tools.advice"))) + .setToolOldVersionErrorHandler( + (name, version) -> new ConfigException( + MessageFormat.format(I18N.getString( + "message.wrong-tool-version"), name, + version, MINIMAL_VERSION), + I18N.getString("error.no-wix-tools.advice"))) + .setVersionParser(output -> { + versionCtnr[0] = ""; + String firstLineOfOutput = output.findFirst().orElse(""); + int separatorIdx = firstLineOfOutput.lastIndexOf(' '); + if (separatorIdx == -1) { + return null; + } + versionCtnr[0] = firstLineOfOutput.substring(separatorIdx + 1); + return versionCtnr[0]; + })::validate; + } + + private final static DottedVersion MINIMAL_VERSION = DottedVersion.lazy("3.0"); + + static Path getSystemDir(String envVar, String knownDir) { + return Optional + .ofNullable(getEnvVariableAsPath(envVar)) + .orElseGet(() -> Optional + .ofNullable(getEnvVariableAsPath("SystemDrive")) + .orElseGet(() -> Path.of("C:")).resolve(knownDir)); + } + + private static Path getEnvVariableAsPath(String envVar) { + String path = System.getenv(envVar); + if (path != null) { + try { + return Path.of(path); + } catch (InvalidPathException ex) { + Log.error(MessageFormat.format(I18N.getString( + "error.invalid-envvar"), envVar)); + } + } + return null; + } + + private static List findWixInstallDirs() { + PathMatcher wixInstallDirMatcher = FileSystems.getDefault().getPathMatcher( + "glob:WiX Toolset v*"); + + Path programFiles = getSystemDir("ProgramFiles", "\\Program Files"); + Path programFilesX86 = getSystemDir("ProgramFiles(x86)", + "\\Program Files (x86)"); + + // Returns list of WiX install directories ordered by WiX version number. + // Newer versions go first. + return Stream.of(programFiles, programFilesX86).map(path -> { + List result; + try (var paths = Files.walk(path, 1)) { + result = paths.collect(Collectors.toList()); + } catch (IOException ex) { + Log.verbose(ex); + result = Collections.emptyList(); + } + return result; + }).flatMap(List::stream) + .filter(path -> wixInstallDirMatcher.matches(path.getFileName())) + .sorted(Comparator.comparing(Path::getFileName).reversed()) + .map(path -> path.resolve("bin")) + .collect(Collectors.toList()); + } +} diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/MsiInstallerStrings_en.wxl b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/MsiInstallerStrings_en.wxl new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/MsiInstallerStrings_en.wxl @@ -0,0 +1,7 @@ + + + The folder [INSTALLDIR] already exist. Would you like to install to that folder anyway? + Main Feature + A higher version of [ProductName] is already installed. Downgrades disabled. Setup will now exit. + A lower version of [ProductName] is already installed. Upgrades disabled. Setup will now exit. + diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/MsiInstallerStrings_ja.wxl b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/MsiInstallerStrings_ja.wxl new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/MsiInstallerStrings_ja.wxl @@ -0,0 +1,7 @@ + + + The folder [INSTALLDIR] already exist. Would you like to install to that folder anyway? + Main Feature + A higher version of [ProductName] is already installed. Downgrades disabled. Setup will now exit. + A lower version of [ProductName] is already installed. Upgrades disabled. Setup will now exit. + diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/MsiInstallerStrings_zh_CN.wxl b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/MsiInstallerStrings_zh_CN.wxl new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/MsiInstallerStrings_zh_CN.wxl @@ -0,0 +1,7 @@ + + + The folder [INSTALLDIR] already exist. Would you like to install to that folder anyway? + Main Feature + A higher version of [ProductName] is already installed. Downgrades disabled. Setup will now exit. + A lower version of [ProductName] is already installed. Upgrades disabled. Setup will now exit. + diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinLauncher.template b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinLauncher.template new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinLauncher.template @@ -0,0 +1,34 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +CompanyName=COMPANY_NAME +FileDescription=FILE_DESCRIPTION +FileVersion=FILE_VERSION +InternalName=INTERNAL_NAME +LegalCopyright=LEGAL_COPYRIGHT +OriginalFilename=ORIGINAL_FILENAME +ProductName=PRODUCT_NAME +ProductVersion=PRODUCT_VERSION diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources.properties b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources.properties @@ -0,0 +1,67 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +app.bundler.name=Windows Application Image +exe.bundler.name=EXE Installer Package +msi.bundler.name=MSI Installer Package + +param.menu-group.default=Unknown + +resource.executable-properties-template=Template for creating executable properties file +resource.setup-icon=setup dialog icon +resource.post-app-image-script=script to run after application image is populated +resource.post-msi-script=script to run after msi file for exe installer is created +resource.wxl-file-name=MsiInstallerStrings_en.wxl +resource.main-wix-file=Main WiX project file +resource.overrides-wix-file=Overrides WiX project file + +error.no-wix-tools=Can not find WiX tools (light.exe, candle.exe) +error.no-wix-tools.advice=Download WiX 3.0 or later from https://wixtoolset.org and add it to the PATH. +error.version-string-wrong-format=Version string is not compatible with MSI rules [{0}] +error.version-string-wrong-format.advice=Set the bundler argument "{0}" according to these rules: https://msdn.microsoft.com/en-us/library/aa370859%28v\=VS.85%29.aspx . +error.version-string-major-out-of-range=Major version must be in the range [0, 255] +error.version-string-build-out-of-range=Build part of version must be in the range [0, 65535] +error.version-string-minor-out-of-range=Minor version must be in the range [0, 255] +error.version-string-part-not-number=Failed to convert version component to int +error.version-swap=Failed to update version information for {0} +error.invalid-envvar=Invalid value of {0} environment variable + +message.result-dir=Result application bundle: {0}. +message.icon-not-ico=The specified icon "{0}" is not an ICO file and will not be used. The default icon will be used in it's place. +message.potential.windows.defender.issue=Warning: Windows Defender may prevent jpackage from functioning. If there is an issue, it can be addressed by either disabling realtime monitoring, or adding an exclusion for the directory "{0}". +message.outputting-to-location=Generating EXE for installer to: {0}. +message.output-location=Installer (.exe) saved to: {0} +message.tool-version=Detected [{0}] version [{1}]. +message.creating-association-with-null-extension=Creating association with null extension. +message.wrong-tool-version=Detected [{0}] version {1} but version {2} is required. +message.version-string-too-many-components=Version sting may have up to 3 components - major.minor.build . +message.use-wix36-features=WiX {0} detected. Enabling advanced cleanup action. +message.product-code=MSI ProductCode: {0}. +message.upgrade-code=MSI UpgradeCode: {0}. +message.preparing-msi-config=Preparing MSI config: {0}. +message.generating-msi=Generating MSI: {0}. +message.invalid.install.dir=Warning: Invalid install directory {0}. Install directory should be a relative sub-path under the default installation location such as "Program Files". Defaulting to application name "{1}". + diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_ja.properties b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_ja.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_ja.properties @@ -0,0 +1,67 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +app.bundler.name=Windows Application Image +exe.bundler.name=EXE Installer Package +msi.bundler.name=MSI Installer Package + +param.menu-group.default=Unknown + +resource.executable-properties-template=Template for creating executable properties file +resource.setup-icon=setup dialog icon +resource.post-app-image-script=script to run after application image is populated +resource.post-msi-script=script to run after msi file for exe installer is created +resource.wxl-file-name=MsiInstallerStrings_en.wxl +resource.main-wix-file=Main WiX project file +resource.overrides-wix-file=Overrides WiX project file + +error.no-wix-tools=Can not find WiX tools (light.exe, candle.exe) +error.no-wix-tools.advice=Download WiX 3.0 or later from https://wixtoolset.org and add it to the PATH. +error.version-string-wrong-format=Version string is not compatible with MSI rules [{0}] +error.version-string-wrong-format.advice=Set the bundler argument "{0}" according to these rules: https://msdn.microsoft.com/en-us/library/aa370859%28v\=VS.85%29.aspx . +error.version-string-major-out-of-range=Major version must be in the range [0, 255] +error.version-string-build-out-of-range=Build part of version must be in the range [0, 65535] +error.version-string-minor-out-of-range=Minor version must be in the range [0, 255] +error.version-string-part-not-number=Failed to convert version component to int +error.version-swap=Failed to update version information for {0} +error.invalid-envvar=Invalid value of {0} environment variable + +message.result-dir=Result application bundle: {0}. +message.icon-not-ico=The specified icon "{0}" is not an ICO file and will not be used. The default icon will be used in it's place. +message.potential.windows.defender.issue=Warning: Windows Defender may prevent jpackage from functioning. If there is an issue, it can be addressed by either disabling realtime monitoring, or adding an exclusion for the directory "{0}". +message.outputting-to-location=Generating EXE for installer to: {0}. +message.output-location=Installer (.exe) saved to: {0} +message.tool-version=Detected [{0}] version [{1}]. +message.creating-association-with-null-extension=Creating association with null extension. +message.wrong-tool-version=Detected [{0}] version {1} but version {2} is required. +message.version-string-too-many-components=Version sting may have up to 3 components - major.minor.build . +message.use-wix36-features=WiX {0} detected. Enabling advanced cleanup action. +message.product-code=MSI ProductCode: {0}. +message.upgrade-code=MSI UpgradeCode: {0}. +message.preparing-msi-config=Preparing MSI config: {0}. +message.generating-msi=Generating MSI: {0}. +message.invalid.install.dir=Warning: Invalid install directory {0}. Install directory should be a relative sub-path under the default installation location such as "Program Files". Defaulting to application name "{1}". + diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_zh_CN.properties b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_zh_CN.properties new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_zh_CN.properties @@ -0,0 +1,67 @@ +# +# Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# + +app.bundler.name=Windows Application Image +exe.bundler.name=EXE Installer Package +msi.bundler.name=MSI Installer Package + +param.menu-group.default=Unknown + +resource.executable-properties-template=Template for creating executable properties file +resource.setup-icon=setup dialog icon +resource.post-app-image-script=script to run after application image is populated +resource.post-msi-script=script to run after msi file for exe installer is created +resource.wxl-file-name=MsiInstallerStrings_en.wxl +resource.main-wix-file=Main WiX project file +resource.overrides-wix-file=Overrides WiX project file + +error.no-wix-tools=Can not find WiX tools (light.exe, candle.exe) +error.no-wix-tools.advice=Download WiX 3.0 or later from https://wixtoolset.org and add it to the PATH. +error.version-string-wrong-format=Version string is not compatible with MSI rules [{0}] +error.version-string-wrong-format.advice=Set the bundler argument "{0}" according to these rules: https://msdn.microsoft.com/en-us/library/aa370859%28v\=VS.85%29.aspx . +error.version-string-major-out-of-range=Major version must be in the range [0, 255] +error.version-string-build-out-of-range=Build part of version must be in the range [0, 65535] +error.version-string-minor-out-of-range=Minor version must be in the range [0, 255] +error.version-string-part-not-number=Failed to convert version component to int +error.version-swap=Failed to update version information for {0} +error.invalid-envvar=Invalid value of {0} environment variable + +message.result-dir=Result application bundle: {0}. +message.icon-not-ico=The specified icon "{0}" is not an ICO file and will not be used. The default icon will be used in it's place. +message.potential.windows.defender.issue=Warning: Windows Defender may prevent jpackage from functioning. If there is an issue, it can be addressed by either disabling realtime monitoring, or adding an exclusion for the directory "{0}". +message.outputting-to-location=Generating EXE for installer to: {0}. +message.output-location=Installer (.exe) saved to: {0} +message.tool-version=Detected [{0}] version [{1}]. +message.creating-association-with-null-extension=Creating association with null extension. +message.wrong-tool-version=Detected [{0}] version {1} but version {2} is required. +message.version-string-too-many-components=Version sting may have up to 3 components - major.minor.build . +message.use-wix36-features=WiX {0} detected. Enabling advanced cleanup action. +message.product-code=MSI ProductCode: {0}. +message.upgrade-code=MSI UpgradeCode: {0}. +message.preparing-msi-config=Preparing MSI config: {0}. +message.generating-msi=Generating MSI: {0}. +message.invalid.install.dir=Warning: Invalid install directory {0}. Install directory should be a relative sub-path under the default installation location such as "Program Files". Defaulting to application name "{1}". + diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/java48.ico b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/java48.ico new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8d2c9571ed9e536a5fc46e043731b65759b928f8 GIT binary patch literal 25214 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 E1DBeGdH?_b diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/main.wxs b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/main.wxs new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/main.wxs @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + 1 + + + !(loc.message.install.dir.exist) + + + + + + + + 1 + INSTALLDIR_VALID="0" + INSTALLDIR_VALID="1" + + + + 1 + 1 + + + + + + + + + + + + + + + + + diff --git a/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/overrides.wxi b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/overrides.wxi new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/overrides.wxi @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/jdk.incubator.jpackage/windows/classes/module-info.java.extra b/src/jdk.incubator.jpackage/windows/classes/module-info.java.extra new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/classes/module-info.java.extra @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +provides jdk.incubator.jpackage.internal.Bundler with + jdk.incubator.jpackage.internal.WinAppBundler, + jdk.incubator.jpackage.internal.WinExeBundler, + jdk.incubator.jpackage.internal.WinMsiBundler; + diff --git a/src/jdk.incubator.jpackage/windows/native/jpackageapplauncher/WinLauncher.cpp b/src/jdk.incubator.jpackage/windows/native/jpackageapplauncher/WinLauncher.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/jpackageapplauncher/WinLauncher.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include +#include +#include +#include + +#define JPACKAGE_LIBRARY TEXT("applauncher.dll") + +typedef bool (*start_launcher)(int argc, TCHAR* argv[]); +typedef void (*stop_launcher)(); + +std::wstring GetTitle() { + std::wstring result; + wchar_t buffer[MAX_PATH]; + GetModuleFileName(NULL, buffer, MAX_PATH - 1); + buffer[MAX_PATH - 1] = '\0'; + result = buffer; + size_t slash = result.find_last_of('\\'); + + if (slash != std::wstring::npos) + result = result.substr(slash + 1, result.size() - slash - 1); + + return result; +} + +#ifdef LAUNCHERC +int main(int argc0, char *argv0[]) { +#else // LAUNCHERC +int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, + LPTSTR lpCmdLine, int nCmdShow) { +#endif // LAUNCHERC + int result = 1; + TCHAR **argv; + int argc; + + // [RT-31061] otherwise UI can be left in back of other windows. + ::AllowSetForegroundWindow(ASFW_ANY); + + ::setlocale(LC_ALL, "en_US.utf8"); + argv = CommandLineToArgvW(GetCommandLine(), &argc); + + HMODULE library = ::LoadLibrary(JPACKAGE_LIBRARY); + + if (library == NULL) { + std::wstring title = GetTitle(); + std::wstring description = std::wstring(JPACKAGE_LIBRARY) + + std::wstring(TEXT(" not found.")); + MessageBox(NULL, description.data(), + title.data(), MB_ICONERROR | MB_OK); + } + else { + start_launcher start = + (start_launcher)GetProcAddress(library, "start_launcher"); + stop_launcher stop = + (stop_launcher)GetProcAddress(library, "stop_launcher"); + + if (start != NULL && stop != NULL) { + if (start(argc, argv) == true) { + result = 0; + stop(); + } + } + + ::FreeLibrary(library); + } + + if (argv != NULL) { + LocalFree(argv); + } + + return result; +} + diff --git a/src/jdk.incubator.jpackage/windows/native/libapplauncher/DllMain.cpp b/src/jdk.incubator.jpackage/windows/native/libapplauncher/DllMain.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libapplauncher/DllMain.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include + +extern "C" { + + BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, + LPVOID lpvReserved) { + return true; + } +} + diff --git a/src/jdk.incubator.jpackage/windows/native/libapplauncher/FileAttribute.h b/src/jdk.incubator.jpackage/windows/native/libapplauncher/FileAttribute.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libapplauncher/FileAttribute.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef FILEATTRIBUTE_H +#define FILEATTRIBUTE_H + +enum FileAttribute { + faArchive = FILE_ATTRIBUTE_ARCHIVE, + faCompressed = FILE_ATTRIBUTE_COMPRESSED, + faDevice = FILE_ATTRIBUTE_DEVICE, + faDirectory = FILE_ATTRIBUTE_DIRECTORY, + faEncrypted = FILE_ATTRIBUTE_ENCRYPTED, + faHidden = FILE_ATTRIBUTE_HIDDEN, + faNormal = FILE_ATTRIBUTE_NORMAL, + faNotContentIndexed = FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, + faOffline = FILE_ATTRIBUTE_OFFLINE, + faSystem = FILE_ATTRIBUTE_SYSTEM, + faSymbolicLink = FILE_ATTRIBUTE_REPARSE_POINT, + faSparceFile = FILE_ATTRIBUTE_SPARSE_FILE, + faReadOnly = FILE_ATTRIBUTE_READONLY, + faTemporary = FILE_ATTRIBUTE_TEMPORARY, + faVirtual = FILE_ATTRIBUTE_VIRTUAL +}; + +#endif // FILEATTRIBUTE_H + diff --git a/src/jdk.incubator.jpackage/windows/native/libapplauncher/FilePath.cpp b/src/jdk.incubator.jpackage/windows/native/libapplauncher/FilePath.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libapplauncher/FilePath.cpp @@ -0,0 +1,468 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "FilePath.h" + +#include +#include +#include + +bool FilePath::FileExists(const TString FileName) { + bool result = false; + WIN32_FIND_DATA FindFileData; + TString fileName = FixPathForPlatform(FileName); + HANDLE handle = FindFirstFile(fileName.data(), &FindFileData); + + if (handle != INVALID_HANDLE_VALUE) { + if (FILE_ATTRIBUTE_DIRECTORY & FindFileData.dwFileAttributes) { + result = true; + } + else { + result = true; + } + + FindClose(handle); + } + return result; +} + +bool FilePath::DirectoryExists(const TString DirectoryName) { + bool result = false; + WIN32_FIND_DATA FindFileData; + TString directoryName = FixPathForPlatform(DirectoryName); + HANDLE handle = FindFirstFile(directoryName.data(), &FindFileData); + + if (handle != INVALID_HANDLE_VALUE) { + if (FILE_ATTRIBUTE_DIRECTORY & FindFileData.dwFileAttributes) { + result = true; + } + + FindClose(handle); + } + return result; +} + +std::string GetLastErrorAsString() { + // Get the error message, if any. + DWORD errorMessageID = ::GetLastError(); + + if (errorMessageID == 0) { + return "No error message has been recorded"; + } + + LPSTR messageBuffer = NULL; + size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER + | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, + SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL); + + std::string message(messageBuffer, size); + + // Free the buffer. + LocalFree(messageBuffer); + + return message; +} + +bool FilePath::DeleteFile(const TString FileName) { + bool result = false; + + if (FileExists(FileName) == true) { + TString lFileName = FixPathForPlatform(FileName); + FileAttributes attributes(lFileName); + + if (attributes.Contains(faReadOnly) == true) { + attributes.Remove(faReadOnly); + } + + result = ::DeleteFile(lFileName.data()) == TRUE; + } + + return result; +} + +bool FilePath::DeleteDirectory(const TString DirectoryName) { + bool result = false; + + if (DirectoryExists(DirectoryName) == true) { + SHFILEOPSTRUCTW fos = {0}; + TString directoryName = FixPathForPlatform(DirectoryName); + DynamicBuffer lDirectoryName(directoryName.size() + 2); + if (lDirectoryName.GetData() == NULL) { + return false; + } + memcpy(lDirectoryName.GetData(), directoryName.data(), + (directoryName.size() + 2) * sizeof(TCHAR)); + lDirectoryName[directoryName.size() + 1] = NULL; + // Double null terminate for SHFileOperation. + + // Delete the folder and everything inside. + fos.wFunc = FO_DELETE; + fos.pFrom = lDirectoryName.GetData(); + fos.fFlags = FOF_NO_UI; + result = SHFileOperation(&fos) == 0; + } + + return result; +} + +TString FilePath::IncludeTrailingSeparator(const TString value) { + TString result = value; + + if (value.size() > 0) { + TString::iterator i = result.end(); + i--; + + if (*i != TRAILING_PATHSEPARATOR) { + result += TRAILING_PATHSEPARATOR; + } + } + + return result; +} + +TString FilePath::IncludeTrailingSeparator(const char* value) { + TString lvalue = PlatformString(value).toString(); + return IncludeTrailingSeparator(lvalue); +} + +TString FilePath::IncludeTrailingSeparator(const wchar_t* value) { + TString lvalue = PlatformString(value).toString(); + return IncludeTrailingSeparator(lvalue); +} + +TString FilePath::ExtractFilePath(TString Path) { + TString result; + size_t slash = Path.find_last_of(TRAILING_PATHSEPARATOR); + if (slash != TString::npos) + result = Path.substr(0, slash); + return result; +} + +TString FilePath::ExtractFileExt(TString Path) { + TString result; + size_t dot = Path.find_last_of('.'); + + if (dot != TString::npos) { + result = Path.substr(dot, Path.size() - dot); + } + + return result; +} + +TString FilePath::ExtractFileName(TString Path) { + TString result; + + size_t slash = Path.find_last_of(TRAILING_PATHSEPARATOR); + if (slash != TString::npos) + result = Path.substr(slash + 1, Path.size() - slash - 1); + + return result; +} + +TString FilePath::ChangeFileExt(TString Path, TString Extension) { + TString result; + size_t dot = Path.find_last_of('.'); + + if (dot != TString::npos) { + result = Path.substr(0, dot) + Extension; + } + + if (result.empty() == true) { + result = Path; + } + + return result; +} + +TString FilePath::FixPathForPlatform(TString Path) { + TString result = Path; + std::replace(result.begin(), result.end(), + BAD_TRAILING_PATHSEPARATOR, TRAILING_PATHSEPARATOR); + // The maximum path that does not require long path prefix. On Windows the + // maximum path is 260 minus 1 (NUL) but for directories it is 260 minus + // 12 minus 1 (to allow for the creation of a 8.3 file in the directory). + const int maxPath = 247; + if (result.length() > maxPath && + result.find(_T("\\\\?\\")) == TString::npos && + result.find(_T("\\\\?\\UNC")) == TString::npos) { + const TString prefix(_T("\\\\")); + if (!result.compare(0, prefix.size(), prefix)) { + // UNC path, converting to UNC path in long notation + result = _T("\\\\?\\UNC") + result.substr(1, result.length()); + } else { + // converting to non-UNC path in long notation + result = _T("\\\\?\\") + result; + } + } + return result; +} + +TString FilePath::FixPathSeparatorForPlatform(TString Path) { + TString result = Path; + std::replace(result.begin(), result.end(), + BAD_PATH_SEPARATOR, PATH_SEPARATOR); + return result; +} + +TString FilePath::PathSeparator() { + TString result; + result = PATH_SEPARATOR; + return result; +} + +bool FilePath::CreateDirectory(TString Path, bool ownerOnly) { + bool result = false; + + std::list paths; + TString lpath = Path; + + while (lpath.empty() == false && DirectoryExists(lpath) == false) { + paths.push_front(lpath); + lpath = ExtractFilePath(lpath); + } + + for (std::list::iterator iterator = paths.begin(); + iterator != paths.end(); iterator++) { + lpath = *iterator; + + if (_wmkdir(lpath.data()) == 0) { + result = true; + } else { + result = false; + break; + } + } + + return result; +} + +void FilePath::ChangePermissions(TString FileName, bool ownerOnly) { +} + +#include + +FileAttributes::FileAttributes(const TString FileName, bool FollowLink) { + FFileName = FileName; + FFollowLink = FollowLink; + ReadAttributes(); +} + +bool FileAttributes::WriteAttributes() { + bool result = false; + + DWORD attributes = 0; + + for (std::vector::const_iterator iterator = + FAttributes.begin(); + iterator != FAttributes.end(); iterator++) { + switch (*iterator) { + case faArchive: { + attributes = attributes & FILE_ATTRIBUTE_ARCHIVE; + break; + } + case faCompressed: { + attributes = attributes & FILE_ATTRIBUTE_COMPRESSED; + break; + } + case faDevice: { + attributes = attributes & FILE_ATTRIBUTE_DEVICE; + break; + } + case faDirectory: { + attributes = attributes & FILE_ATTRIBUTE_DIRECTORY; + break; + } + case faEncrypted: { + attributes = attributes & FILE_ATTRIBUTE_ENCRYPTED; + break; + } + case faHidden: { + attributes = attributes & FILE_ATTRIBUTE_HIDDEN; + break; + } + case faNormal: { + attributes = attributes & FILE_ATTRIBUTE_NORMAL; + break; + } + case faNotContentIndexed: { + attributes = attributes & FILE_ATTRIBUTE_NOT_CONTENT_INDEXED; + break; + } + case faOffline: { + attributes = attributes & FILE_ATTRIBUTE_OFFLINE; + break; + } + case faSystem: { + attributes = attributes & FILE_ATTRIBUTE_SYSTEM; + break; + } + case faSymbolicLink: { + attributes = attributes & FILE_ATTRIBUTE_REPARSE_POINT; + break; + } + case faSparceFile: { + attributes = attributes & FILE_ATTRIBUTE_SPARSE_FILE; + break; + } + case faReadOnly: { + attributes = attributes & FILE_ATTRIBUTE_READONLY; + break; + } + case faTemporary: { + attributes = attributes & FILE_ATTRIBUTE_TEMPORARY; + break; + } + case faVirtual: { + attributes = attributes & FILE_ATTRIBUTE_VIRTUAL; + break; + } + } + } + + if (::SetFileAttributes(FFileName.data(), attributes) != 0) { + result = true; + } + + return result; +} + +#define S_ISRUSR(m) (((m) & S_IRWXU) == S_IRUSR) +#define S_ISWUSR(m) (((m) & S_IRWXU) == S_IWUSR) +#define S_ISXUSR(m) (((m) & S_IRWXU) == S_IXUSR) + +#define S_ISRGRP(m) (((m) & S_IRWXG) == S_IRGRP) +#define S_ISWGRP(m) (((m) & S_IRWXG) == S_IWGRP) +#define S_ISXGRP(m) (((m) & S_IRWXG) == S_IXGRP) + +#define S_ISROTH(m) (((m) & S_IRWXO) == S_IROTH) +#define S_ISWOTH(m) (((m) & S_IRWXO) == S_IWOTH) +#define S_ISXOTH(m) (((m) & S_IRWXO) == S_IXOTH) + +bool FileAttributes::ReadAttributes() { + bool result = false; + + DWORD attributes = ::GetFileAttributes(FFileName.data()); + + if (attributes != INVALID_FILE_ATTRIBUTES) { + result = true; + + if (attributes | FILE_ATTRIBUTE_ARCHIVE) { + FAttributes.push_back(faArchive); + } + if (attributes | FILE_ATTRIBUTE_COMPRESSED) { + FAttributes.push_back(faCompressed); + } + if (attributes | FILE_ATTRIBUTE_DEVICE) { + FAttributes.push_back(faDevice); + } + if (attributes | FILE_ATTRIBUTE_DIRECTORY) { + FAttributes.push_back(faDirectory); + } + if (attributes | FILE_ATTRIBUTE_ENCRYPTED) { + FAttributes.push_back(faEncrypted); + } + if (attributes | FILE_ATTRIBUTE_HIDDEN) { + FAttributes.push_back(faHidden); + } + if (attributes | FILE_ATTRIBUTE_NORMAL) { + FAttributes.push_back(faNormal); + } + if (attributes | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED) { + FAttributes.push_back(faNotContentIndexed); + } + if (attributes | FILE_ATTRIBUTE_SYSTEM) { + FAttributes.push_back(faSystem); + } + if (attributes | FILE_ATTRIBUTE_OFFLINE) { + FAttributes.push_back(faOffline); + } + if (attributes | FILE_ATTRIBUTE_REPARSE_POINT) { + FAttributes.push_back(faSymbolicLink); + } + if (attributes | FILE_ATTRIBUTE_SPARSE_FILE) { + FAttributes.push_back(faSparceFile); + } + if (attributes | FILE_ATTRIBUTE_READONLY ) { + FAttributes.push_back(faReadOnly); + } + if (attributes | FILE_ATTRIBUTE_TEMPORARY) { + FAttributes.push_back(faTemporary); + } + if (attributes | FILE_ATTRIBUTE_VIRTUAL) { + FAttributes.push_back(faVirtual); + } + } + + return result; +} + +bool FileAttributes::Valid(const FileAttribute Value) { + bool result = false; + + switch (Value) { + case faHidden: + case faReadOnly: { + result = true; + break; + } + default: + break; + } + + return result; +} + +void FileAttributes::Append(FileAttribute Value) { + if (Valid(Value) == true) { + FAttributes.push_back(Value); + WriteAttributes(); + } +} + +bool FileAttributes::Contains(FileAttribute Value) { + bool result = false; + + std::vector::const_iterator iterator = + std::find(FAttributes.begin(), FAttributes.end(), Value); + + if (iterator != FAttributes.end()) { + result = true; + } + + return result; +} + +void FileAttributes::Remove(FileAttribute Value) { + if (Valid(Value) == true) { + std::vector::iterator iterator = + std::find(FAttributes.begin(), FAttributes.end(), Value); + + if (iterator != FAttributes.end()) { + FAttributes.erase(iterator); + WriteAttributes(); + } + } +} diff --git a/src/jdk.incubator.jpackage/windows/native/libapplauncher/PlatformDefs.h b/src/jdk.incubator.jpackage/windows/native/libapplauncher/PlatformDefs.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libapplauncher/PlatformDefs.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef PLATFORM_DEFS_H +#define PLATFORM_DEFS_H + +// Define Windows compatibility requirements XP or later +#define WINVER 0x0600 +#define _WIN32_WINNT 0x0600 + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +#ifndef WINDOWS +#define WINDOWS +#endif + +typedef std::wstring TString; +#define StringLength wcslen + +#define TRAILING_PATHSEPARATOR '\\' +#define BAD_TRAILING_PATHSEPARATOR '/' +#define PATH_SEPARATOR ';' +#define BAD_PATH_SEPARATOR ':' + +typedef ULONGLONG TPlatformNumber; +typedef DWORD TProcessID; + +typedef void* Module; +typedef void* Procedure; + +#endif // PLATFORM_DEFS_H diff --git a/src/jdk.incubator.jpackage/windows/native/libapplauncher/WindowsPlatform.cpp b/src/jdk.incubator.jpackage/windows/native/libapplauncher/WindowsPlatform.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libapplauncher/WindowsPlatform.cpp @@ -0,0 +1,759 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Platform.h" + +#include "JavaVirtualMachine.h" +#include "WindowsPlatform.h" +#include "Package.h" +#include "Helpers.h" +#include "PlatformString.h" +#include "Macros.h" + +#include +#include +#include +#include +#include +#include + +using namespace std; + +#define WINDOWS_JPACKAGE_TMP_DIR \ + L"\\AppData\\Local\\Java\\JPackage\\tmp" + +class Registry { +private: + HKEY FKey; + HKEY FOpenKey; + bool FOpen; + +public: + + Registry(HKEY Key) { + FOpen = false; + FKey = Key; + } + + ~Registry() { + Close(); + } + + void Close() { + if (FOpen == true) { + RegCloseKey(FOpenKey); + } + } + + bool Open(TString SubKey) { + bool result = false; + Close(); + + if (RegOpenKeyEx(FKey, SubKey.data(), 0, KEY_READ, &FOpenKey) == + ERROR_SUCCESS) { + result = true; + } + + return result; + } + + std::list GetKeys() { + std::list result; + DWORD count; + + if (RegQueryInfoKey(FOpenKey, NULL, NULL, NULL, NULL, NULL, NULL, + &count, NULL, NULL, NULL, NULL) == ERROR_SUCCESS) { + + DWORD length = 255; + DynamicBuffer buffer(length); + if (buffer.GetData() == NULL) { + return result; + } + + for (unsigned int index = 0; index < count; index++) { + buffer.Zero(); + DWORD status = RegEnumValue(FOpenKey, index, buffer.GetData(), + &length, NULL, NULL, NULL, NULL); + + while (status == ERROR_MORE_DATA) { + length = length * 2; + if (!buffer.Resize(length)) { + return result; + } + status = RegEnumValue(FOpenKey, index, buffer.GetData(), + &length, NULL, NULL, NULL, NULL); + } + + if (status == ERROR_SUCCESS) { + TString value = buffer.GetData(); + result.push_back(value); + } + } + } + + return result; + } + + TString ReadString(TString Name) { + TString result; + DWORD length; + DWORD dwRet; + DynamicBuffer buffer(0); + length = 0; + + dwRet = RegQueryValueEx(FOpenKey, Name.data(), NULL, NULL, NULL, + &length); + if (dwRet == ERROR_MORE_DATA || dwRet == 0) { + if (!buffer.Resize(length + 1)) { + return result; + } + dwRet = RegQueryValueEx(FOpenKey, Name.data(), NULL, NULL, + (LPBYTE) buffer.GetData(), &length); + result = buffer.GetData(); + } + + return result; + } +}; + +WindowsPlatform::WindowsPlatform(void) : Platform() { + FMainThread = ::GetCurrentThreadId(); +} + +WindowsPlatform::~WindowsPlatform(void) { +} + +TString WindowsPlatform::GetPackageAppDirectory() { + return FilePath::IncludeTrailingSeparator( + GetPackageRootDirectory()) + _T("app"); +} + +TString WindowsPlatform::GetPackageLauncherDirectory() { + return GetPackageRootDirectory(); +} + +TString WindowsPlatform::GetPackageRuntimeBinDirectory() { + return FilePath::IncludeTrailingSeparator(GetPackageRootDirectory()) + _T("runtime\\bin"); +} + +TCHAR* WindowsPlatform::ConvertStringToFileSystemString(TCHAR* Source, + bool &release) { + // Not Implemented. + return NULL; +} + +TCHAR* WindowsPlatform::ConvertFileSystemStringToString(TCHAR* Source, + bool &release) { + // Not Implemented. + return NULL; +} + +TString WindowsPlatform::GetPackageRootDirectory() { + TString result; + TString filename = GetModuleFileName(); + return FilePath::ExtractFilePath(filename); +} + +TString WindowsPlatform::GetAppDataDirectory() { + TString result; + TCHAR path[MAX_PATH]; + + if (SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, path) == S_OK) { + result = path; + } + + return result; +} + +TString WindowsPlatform::GetAppName() { + TString result = GetModuleFileName(); + result = FilePath::ExtractFileName(result); + result = FilePath::ChangeFileExt(result, _T("")); + return result; +} + +void WindowsPlatform::ShowMessage(TString title, TString description) { + MessageBox(NULL, description.data(), + !title.empty() ? title.data() : description.data(), + MB_ICONERROR | MB_OK); +} + +void WindowsPlatform::ShowMessage(TString description) { + TString appname = GetModuleFileName(); + appname = FilePath::ExtractFileName(appname); + MessageBox(NULL, description.data(), appname.data(), MB_ICONERROR | MB_OK); +} + +MessageResponse WindowsPlatform::ShowResponseMessage(TString title, + TString description) { + MessageResponse result = mrCancel; + + if (::MessageBox(NULL, description.data(), title.data(), MB_OKCANCEL) == + IDOK) { + result = mrOK; + } + + return result; +} + +TString WindowsPlatform::GetBundledJavaLibraryFileName(TString RuntimePath) { + TString result = FilePath::IncludeTrailingSeparator(RuntimePath) + + _T("jre\\bin\\jli.dll"); + + if (FilePath::FileExists(result) == false) { + result = FilePath::IncludeTrailingSeparator(RuntimePath) + + _T("bin\\jli.dll"); + } + + return result; +} + +ISectionalPropertyContainer* WindowsPlatform::GetConfigFile(TString FileName) { + IniFile *result = new IniFile(); + if (result == NULL) { + return NULL; + } + + result->LoadFromFile(FileName); + + return result; +} + +TString WindowsPlatform::GetModuleFileName() { + TString result; + DynamicBuffer buffer(MAX_PATH); + if (buffer.GetData() == NULL) { + return result; + } + + ::GetModuleFileName(NULL, buffer.GetData(), + static_cast (buffer.GetSize())); + + while (ERROR_INSUFFICIENT_BUFFER == GetLastError()) { + if (!buffer.Resize(buffer.GetSize() * 2)) { + return result; + } + ::GetModuleFileName(NULL, buffer.GetData(), + static_cast (buffer.GetSize())); + } + + result = buffer.GetData(); + return result; +} + +Module WindowsPlatform::LoadLibrary(TString FileName) { + return ::LoadLibrary(FileName.data()); +} + +void WindowsPlatform::FreeLibrary(Module AModule) { + ::FreeLibrary((HMODULE) AModule); +} + +Procedure WindowsPlatform::GetProcAddress(Module AModule, + std::string MethodName) { + return ::GetProcAddress((HMODULE) AModule, MethodName.c_str()); +} + +bool WindowsPlatform::IsMainThread() { + bool result = (FMainThread == ::GetCurrentThreadId()); + return result; +} + +TString WindowsPlatform::GetTempDirectory() { + TString result; + PWSTR userDir = 0; + + if (SUCCEEDED(SHGetKnownFolderPath( + FOLDERID_Profile, + 0, + NULL, + &userDir))) { + result = userDir; + result += WINDOWS_JPACKAGE_TMP_DIR; + CoTaskMemFree(userDir); + } + + return result; +} + +static BOOL CALLBACK enumWindows(HWND winHandle, LPARAM lParam) { + DWORD pid = (DWORD) lParam, wPid = 0; + GetWindowThreadProcessId(winHandle, &wPid); + if (pid == wPid) { + SetForegroundWindow(winHandle); + return FALSE; + } + return TRUE; +} + +TPlatformNumber WindowsPlatform::GetMemorySize() { + SYSTEM_INFO si; + GetSystemInfo(&si); + size_t result = (size_t) si.lpMaximumApplicationAddress; + result = result / 1048576; // Convert from bytes to megabytes. + return result; +} + +std::vector FilterList(std::vector &Items, + std::wregex Pattern) { + std::vector result; + + for (std::vector::iterator it = Items.begin(); + it != Items.end(); ++it) { + TString item = *it; + std::wsmatch match; + + if (std::regex_search(item, match, Pattern)) { + result.push_back(item); + } + } + return result; +} + +Process* WindowsPlatform::CreateProcess() { + return new WindowsProcess(); +} + +void WindowsPlatform::InitStreamLocale(wios *stream) { + const std::locale empty_locale = std::locale::empty(); + const std::locale utf8_locale = + std::locale(empty_locale, new std::codecvt_utf8()); + stream->imbue(utf8_locale); +} + +void WindowsPlatform::addPlatformDependencies(JavaLibrary *pJavaLibrary) { + if (pJavaLibrary == NULL) { + return; + } + + if (FilePath::FileExists(_T("msvcr100.dll")) == true) { + pJavaLibrary->AddDependency(_T("msvcr100.dll")); + } + + TString runtimeBin = GetPackageRuntimeBinDirectory(); + SetDllDirectory(runtimeBin.c_str()); +} + +void Platform::CopyString(char *Destination, + size_t NumberOfElements, const char *Source) { + strcpy_s(Destination, NumberOfElements, Source); + + if (NumberOfElements > 0) { + Destination[NumberOfElements - 1] = '\0'; + } +} + +void Platform::CopyString(wchar_t *Destination, + size_t NumberOfElements, const wchar_t *Source) { + wcscpy_s(Destination, NumberOfElements, Source); + + if (NumberOfElements > 0) { + Destination[NumberOfElements - 1] = '\0'; + } +} + +// Owner must free the return value. +MultibyteString Platform::WideStringToMultibyteString( + const wchar_t* value) { + MultibyteString result; + size_t count = 0; + + if (value == NULL) { + return result; + } + + count = WideCharToMultiByte(CP_UTF8, 0, value, -1, NULL, 0, NULL, NULL); + + if (count > 0) { + result.data = new char[count + 1]; + result.length = WideCharToMultiByte(CP_UTF8, 0, value, -1, + result.data, (int)count, NULL, NULL); + } + + return result; +} + +// Owner must free the return value. +WideString Platform::MultibyteStringToWideString(const char* value) { + WideString result; + size_t count = 0; + + if (value == NULL) { + return result; + } + + mbstowcs_s(&count, NULL, 0, value, _TRUNCATE); + + if (count > 0) { + result.data = new wchar_t[count + 1]; + mbstowcs_s(&result.length, result.data, count, value, count); + } + + return result; +} + +FileHandle::FileHandle(std::wstring FileName) { + FHandle = ::CreateFile(FileName.data(), GENERIC_READ, FILE_SHARE_READ, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); +} + +FileHandle::~FileHandle() { + if (IsValid() == true) { + ::CloseHandle(FHandle); + } +} + +bool FileHandle::IsValid() { + return FHandle != INVALID_HANDLE_VALUE; +} + +HANDLE FileHandle::GetHandle() { + return FHandle; +} + +FileMappingHandle::FileMappingHandle(HANDLE FileHandle) { + FHandle = ::CreateFileMapping(FileHandle, NULL, PAGE_READONLY, 0, 0, NULL); +} + +bool FileMappingHandle::IsValid() { + return FHandle != NULL; +} + +FileMappingHandle::~FileMappingHandle() { + if (IsValid() == true) { + ::CloseHandle(FHandle); + } +} + +HANDLE FileMappingHandle::GetHandle() { + return FHandle; +} + +FileData::FileData(HANDLE Handle) { + FBaseAddress = ::MapViewOfFile(Handle, FILE_MAP_READ, 0, 0, 0); +} + +FileData::~FileData() { + if (IsValid() == true) { + ::UnmapViewOfFile(FBaseAddress); + } +} + +bool FileData::IsValid() { + return FBaseAddress != NULL; +} + +LPVOID FileData::GetBaseAddress() { + return FBaseAddress; +} + +WindowsLibrary::WindowsLibrary(std::wstring FileName) { + FFileName = FileName; +} + +std::vector WindowsLibrary::GetImports() { + std::vector result; + FileHandle library(FFileName); + + if (library.IsValid() == true) { + FileMappingHandle mapping(library.GetHandle()); + + if (mapping.IsValid() == true) { + FileData fileData(mapping.GetHandle()); + + if (fileData.IsValid() == true) { + PIMAGE_DOS_HEADER dosHeader = + (PIMAGE_DOS_HEADER) fileData.GetBaseAddress(); + PIMAGE_FILE_HEADER pImgFileHdr = + (PIMAGE_FILE_HEADER) fileData.GetBaseAddress(); + if (dosHeader->e_magic == IMAGE_DOS_SIGNATURE) { + result = DumpPEFile(dosHeader); + } + } + } + } + + return result; +} + +// Given an RVA, look up the section header that encloses it and return a +// pointer to its IMAGE_SECTION_HEADER + +PIMAGE_SECTION_HEADER WindowsLibrary::GetEnclosingSectionHeader(DWORD rva, + PIMAGE_NT_HEADERS pNTHeader) { + PIMAGE_SECTION_HEADER result = 0; + PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(pNTHeader); + + for (unsigned index = 0; index < pNTHeader->FileHeader.NumberOfSections; + index++, section++) { + // Is the RVA is within this section? + if ((rva >= section->VirtualAddress) && + (rva < (section->VirtualAddress + section->Misc.VirtualSize))) { + result = section; + } + } + + return result; +} + +LPVOID WindowsLibrary::GetPtrFromRVA(DWORD rva, PIMAGE_NT_HEADERS pNTHeader, + DWORD imageBase) { + LPVOID result = 0; + PIMAGE_SECTION_HEADER pSectionHdr = GetEnclosingSectionHeader(rva, + pNTHeader); + + if (pSectionHdr != NULL) { + INT delta = (INT) ( + pSectionHdr->VirtualAddress - pSectionHdr->PointerToRawData); + DWORD_PTR dwp = (DWORD_PTR) (imageBase + rva - delta); + result = reinterpret_cast (dwp); // VS2017 - FIXME + } + + return result; +} + +std::vector WindowsLibrary::GetImportsSection(DWORD base, + PIMAGE_NT_HEADERS pNTHeader) { + std::vector result; + + // Look up where the imports section is located. Normally in + // the .idata section, + // but not necessarily so. Therefore, grab the RVA from the data dir. + DWORD importsStartRVA = pNTHeader->OptionalHeader.DataDirectory[ + IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; + + if (importsStartRVA != NULL) { + // Get the IMAGE_SECTION_HEADER that contains the imports. This is + // usually the .idata section, but doesn't have to be. + PIMAGE_SECTION_HEADER pSection = + GetEnclosingSectionHeader(importsStartRVA, pNTHeader); + + if (pSection != NULL) { + PIMAGE_IMPORT_DESCRIPTOR importDesc = + (PIMAGE_IMPORT_DESCRIPTOR) GetPtrFromRVA( + importsStartRVA, pNTHeader, base); + + if (importDesc != NULL) { + while (true) { + // See if we've reached an empty IMAGE_IMPORT_DESCRIPTOR + if ((importDesc->TimeDateStamp == 0) && + (importDesc->Name == 0)) { + break; + } + + std::string filename = (char*) GetPtrFromRVA( + importDesc->Name, pNTHeader, base); + result.push_back(PlatformString(filename)); + importDesc++; // advance to next IMAGE_IMPORT_DESCRIPTOR + } + } + } + } + + return result; +} + +std::vector WindowsLibrary::DumpPEFile(PIMAGE_DOS_HEADER dosHeader) { + std::vector result; + // all of this is VS2017 - FIXME + DWORD_PTR dwDosHeaders = reinterpret_cast (dosHeader); + DWORD_PTR dwPIHeaders = dwDosHeaders + (DWORD) (dosHeader->e_lfanew); + + PIMAGE_NT_HEADERS pNTHeader = + reinterpret_cast (dwPIHeaders); + + // Verify that the e_lfanew field gave us a reasonable + // pointer and the PE signature. + // TODO: To really fix JDK-8131321 this condition needs to be changed. + // There is a matching change + // in JavaVirtualMachine.cpp that also needs to be changed. + if (pNTHeader->Signature == IMAGE_NT_SIGNATURE) { + DWORD base = (DWORD) (dwDosHeaders); + result = GetImportsSection(base, pNTHeader); + } + + return result; +} + +#include + +WindowsJob::WindowsJob() { + FHandle = NULL; +} + +WindowsJob::~WindowsJob() { + if (FHandle != NULL) { + CloseHandle(FHandle); + } +} + +HANDLE WindowsJob::GetHandle() { + if (FHandle == NULL) { + FHandle = CreateJobObject(NULL, NULL); // GLOBAL + + if (FHandle == NULL) { + ::MessageBox(0, _T("Could not create job object"), + _T("TEST"), MB_OK); + } else { + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = {0}; + + // Configure all child processes associated with + // the job to terminate when the + jeli.BasicLimitInformation.LimitFlags = + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + if (0 == SetInformationJobObject(FHandle, + JobObjectExtendedLimitInformation, &jeli, sizeof (jeli))) { + ::MessageBox(0, _T("Could not SetInformationJobObject"), + _T("TEST"), MB_OK); + } + } + } + + return FHandle; +} + +// Initialize static member of WindowsProcess +WindowsJob WindowsProcess::FJob; + +WindowsProcess::WindowsProcess() : Process() { + FRunning = false; +} + +WindowsProcess::~WindowsProcess() { + Terminate(); +} + +void WindowsProcess::Cleanup() { + CloseHandle(FProcessInfo.hProcess); + CloseHandle(FProcessInfo.hThread); +} + +bool WindowsProcess::IsRunning() { + bool result = false; + + HANDLE handle = ::CreateToolhelp32Snapshot(TH32CS_SNAPALL, 0); + if (handle == INVALID_HANDLE_VALUE) { + return false; + } + + PROCESSENTRY32 process = {0}; + process.dwSize = sizeof (process); + + if (::Process32First(handle, &process)) { + do { + if (process.th32ProcessID == FProcessInfo.dwProcessId) { + result = true; + break; + } + } while (::Process32Next(handle, &process)); + } + + CloseHandle(handle); + + return result; +} + +bool WindowsProcess::Terminate() { + bool result = false; + + if (IsRunning() == true && FRunning == true) { + FRunning = false; + } + + return result; +} + +bool WindowsProcess::Execute(const TString Application, + const std::vector Arguments, bool AWait) { + bool result = false; + + if (FRunning == false) { + FRunning = true; + + STARTUPINFO startupInfo; + ZeroMemory(&startupInfo, sizeof (startupInfo)); + startupInfo.cb = sizeof (startupInfo); + ZeroMemory(&FProcessInfo, sizeof (FProcessInfo)); + + TString command = Application; + + for (std::vector::const_iterator iterator = Arguments.begin(); + iterator != Arguments.end(); iterator++) { + command += TString(_T(" ")) + *iterator; + } + + if (::CreateProcess(Application.data(), (wchar_t*)command.data(), NULL, + NULL, FALSE, 0, NULL, NULL, &startupInfo, &FProcessInfo) + == FALSE) { + TString message = PlatformString::Format( + _T("Error: Unable to create process %s"), + Application.data()); + throw Exception(message); + } else { + if (FJob.GetHandle() != NULL) { + if (::AssignProcessToJobObject(FJob.GetHandle(), + FProcessInfo.hProcess) == 0) { + // Failed to assign process to job. It doesn't prevent + // anything from continuing so continue. + } + } + + // Wait until child process exits. + if (AWait == true) { + Wait(); + // Close process and thread handles. + Cleanup(); + } + } + } + + return result; +} + +bool WindowsProcess::Wait() { + bool result = false; + + WaitForSingleObject(FProcessInfo.hProcess, INFINITE); + return result; +} + +TProcessID WindowsProcess::GetProcessID() { + return FProcessInfo.dwProcessId; +} + +bool WindowsProcess::ReadOutput() { + bool result = false; + // TODO implement + return result; +} + +void WindowsProcess::SetInput(TString Value) { + // TODO implement +} + +std::list WindowsProcess::GetOutput() { + ReadOutput(); + return Process::GetOutput(); +} diff --git a/src/jdk.incubator.jpackage/windows/native/libapplauncher/WindowsPlatform.h b/src/jdk.incubator.jpackage/windows/native/libapplauncher/WindowsPlatform.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libapplauncher/WindowsPlatform.h @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef WINDOWSPLATFORM_H +#define WINDOWSPLATFORM_H + +#include +#include "Platform.h" + +class WindowsPlatform : virtual public Platform { +private: + DWORD FMainThread; + +public: + WindowsPlatform(void); + virtual ~WindowsPlatform(void); + + virtual TCHAR* ConvertStringToFileSystemString(TCHAR* Source, + bool &release); + virtual TCHAR* ConvertFileSystemStringToString(TCHAR* Source, + bool &release); + + virtual void ShowMessage(TString title, TString description); + virtual void ShowMessage(TString description); + virtual MessageResponse ShowResponseMessage(TString title, + TString description); + + virtual TString GetPackageRootDirectory(); + virtual TString GetAppDataDirectory(); + virtual TString GetAppName(); + virtual TString GetBundledJavaLibraryFileName(TString RuntimePath); + TString GetPackageAppDirectory(); + TString GetPackageLauncherDirectory(); + TString GetPackageRuntimeBinDirectory(); + + virtual ISectionalPropertyContainer* GetConfigFile(TString FileName); + + virtual TString GetModuleFileName(); + virtual Module LoadLibrary(TString FileName); + virtual void FreeLibrary(Module AModule); + virtual Procedure GetProcAddress(Module AModule, std::string MethodName); + + virtual Process* CreateProcess(); + + virtual bool IsMainThread(); + virtual TPlatformNumber GetMemorySize(); + + virtual TString GetTempDirectory(); + void InitStreamLocale(wios *stream); + void addPlatformDependencies(JavaLibrary *pJavaLibrary); +}; + +class FileHandle { +private: + HANDLE FHandle; + +public: + FileHandle(std::wstring FileName); + ~FileHandle(); + + bool IsValid(); + HANDLE GetHandle(); +}; + + +class FileMappingHandle { +private: + HANDLE FHandle; + +public: + FileMappingHandle(HANDLE FileHandle); + ~FileMappingHandle(); + + bool IsValid(); + HANDLE GetHandle(); +}; + + +class FileData { +private: + LPVOID FBaseAddress; + +public: + FileData(HANDLE Handle); + ~FileData(); + + bool IsValid(); + LPVOID GetBaseAddress(); +}; + + +class WindowsLibrary { +private: + TString FFileName; + + // Given an RVA, look up the section header that encloses it and return a + // pointer to its IMAGE_SECTION_HEADER + static PIMAGE_SECTION_HEADER GetEnclosingSectionHeader(DWORD rva, + PIMAGE_NT_HEADERS pNTHeader); + static LPVOID GetPtrFromRVA(DWORD rva, PIMAGE_NT_HEADERS pNTHeader, + DWORD imageBase); + static std::vector GetImportsSection(DWORD base, + PIMAGE_NT_HEADERS pNTHeader); + static std::vector DumpPEFile(PIMAGE_DOS_HEADER dosHeader); + +public: + WindowsLibrary(const TString FileName); + + std::vector GetImports(); +}; + + +class WindowsJob { +private: + HANDLE FHandle; + +public: + WindowsJob(); + ~WindowsJob(); + + HANDLE GetHandle(); +}; + + +class WindowsProcess : public Process { +private: + bool FRunning; + + PROCESS_INFORMATION FProcessInfo; + static WindowsJob FJob; + + void Cleanup(); + bool ReadOutput(); + +public: + WindowsProcess(); + virtual ~WindowsProcess(); + + virtual bool IsRunning(); + virtual bool Terminate(); + virtual bool Execute(const TString Application, + const std::vector Arguments, bool AWait = false); + virtual bool Wait(); + virtual TProcessID GetProcessID(); + virtual void SetInput(TString Value); + virtual std::list GetOutput(); +}; + +#endif // WINDOWSPLATFORM_H diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/ByteBuffer.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/ByteBuffer.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/ByteBuffer.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "ByteBuffer.h" + +#include + +ByteBuffer::ByteBuffer() { + buffer.reserve(1024); +} + +ByteBuffer::~ByteBuffer() { +} + +LPBYTE ByteBuffer::getPtr() { + return &buffer[0]; +} + +size_t ByteBuffer::getPos() { + return buffer.size(); +} + +void ByteBuffer::AppendString(wstring str) { + size_t len = (str.size() + 1) * sizeof (WCHAR); + AppendBytes((BYTE*) str.c_str(), len); +} + +void ByteBuffer::AppendWORD(WORD word) { + AppendBytes((BYTE*) & word, sizeof (WORD)); +} + +void ByteBuffer::Align(size_t bytesNumber) { + size_t pos = getPos(); + if (pos % bytesNumber) { + DWORD dwNull = 0; + size_t len = bytesNumber - pos % bytesNumber; + AppendBytes((BYTE*) & dwNull, len); + } +} + +void ByteBuffer::AppendBytes(BYTE* ptr, size_t len) { + buffer.insert(buffer.end(), ptr, ptr + len); +} + +void ByteBuffer::ReplaceWORD(size_t offset, WORD word) { + ReplaceBytes(offset, (BYTE*) & word, sizeof (WORD)); +} + +void ByteBuffer::ReplaceBytes(size_t offset, BYTE* ptr, size_t len) { + for (size_t i = 0; i < len; i++) { + buffer[offset + i] = *(ptr + i); + } +} diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/ByteBuffer.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/ByteBuffer.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/ByteBuffer.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef BYTEBUFFER_H +#define BYTEBUFFER_H + +#include +#include +#include + +using namespace std; + +class ByteBuffer { +public: + ByteBuffer(); + ~ByteBuffer(); + + LPBYTE getPtr(); + size_t getPos(); + + void AppendString(wstring str); + void AppendWORD(WORD word); + void AppendBytes(BYTE* ptr, size_t len); + + void ReplaceWORD(size_t offset, WORD word); + void ReplaceBytes(size_t offset, BYTE* ptr, size_t len); + + void Align(size_t bytesNumber); + +private: + vector buffer; +}; + +#endif // BYTEBUFFER_H + diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/ErrorHandling.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/ErrorHandling.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/ErrorHandling.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include + +#include "ErrorHandling.h" +#include "Log.h" + + +namespace { + +tstring getFilename(const SourceCodePos& pos) { + const std::string buf(pos.file); + const std::string::size_type idx = buf.find_last_of("\\/"); + if (idx == std::string::npos) { + return tstrings::fromUtf8(buf); + } + return tstrings::fromUtf8(buf.substr(idx + 1)); +} + +void reportError(const SourceCodePos& pos, const tstring& msg) { + Logger::defaultLogger().log(Logger::LOG_ERROR, getFilename(pos).c_str(), + pos.lno, tstrings::fromUtf8(pos.func).c_str(), msg); +} + +} // namespace + +void reportError(const SourceCodePos& pos, const std::exception& e) { + reportError(pos, (tstrings::any() << "Exception with message \'" + << e.what() << "\' caught").tstr()); +} + + +void reportUnknownError(const SourceCodePos& pos) { + reportError(pos, _T("Unknown exception caught")); +} + + +std::string makeMessage(const std::exception& e, const SourceCodePos& pos) { + std::ostringstream printer; + printer << getFilename(pos) << "(" << pos.lno << ") at " + << pos.func << "(): " + << e.what(); + return printer.str(); +} + + +namespace { + +bool isNotSpace(int chr) { + return isspace(chr) == 0; +} + + +enum TrimMode { + TrimLeading = 0x10, + TrimTrailing = 0x20, + TrimBoth = TrimLeading | TrimTrailing +}; + +// Returns position of the last printed character in the given string. +// Returns std::string::npos if nothing was printed. +size_t printWithoutWhitespaces(std::ostream& out, const std::string& str, + TrimMode mode) { + std::string::const_reverse_iterator it = str.rbegin(); + std::string::const_reverse_iterator end = str.rend(); + + if (mode & TrimLeading) { + // skip leading whitespace + std::string::const_iterator entry = std::find_if(str.begin(), + str.end(), isNotSpace); + end = std::string::const_reverse_iterator(entry); + } + + if (mode & TrimTrailing) { + // skip trailing whitespace + it = std::find_if(it, end, isNotSpace); + } + + if (it == end) { + return std::string::npos; + } + + const size_t pos = str.rend() - end; + const size_t len = end - it; + out.write(str.c_str() + pos, len); + return pos + len - 1; +} + +} // namespace + +std::string joinErrorMessages(const std::string& a, const std::string& b) { + const std::string endPhraseChars(";.,:!?"); + const std::string space(" "); + const std::string dotAndSpace(". "); + + std::ostringstream printer; + printer.exceptions(std::ios::failbit | std::ios::badbit); + + size_t idx = printWithoutWhitespaces(printer, a, TrimTrailing); + size_t extra = 0; + if (idx < a.size() && endPhraseChars.find(a[idx]) == std::string::npos) { + printer << dotAndSpace; + extra = dotAndSpace.size(); + } else if (idx != std::string::npos) { + printer << space; + extra = space.size(); + } + + idx = printWithoutWhitespaces(printer, b, TrimBoth); + + const std::string str = printer.str(); + + if (std::string::npos == idx && extra) { + // Nothing printed from the 'b' message. Backout delimiter string. + return str.substr(0, str.size() - extra); + } + return str; +} diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/ErrorHandling.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/ErrorHandling.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/ErrorHandling.h @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef ErrorHandling_h +#define ErrorHandling_h + + +#include + +#include "SourceCodePos.h" +#include "tstrings.h" + + +// +// Exception handling helpers. Allow transparent exception logging. +// Use as follows: +// +// void foo () { +// JP_TRY; +// +// if (!do_something()) { +// JP_THROW("do_something() failed"); +// } +// +// JP_CATCH_ALL; +// } +// + + +// Logs std::exception caught at 'pos'. +void reportError(const SourceCodePos& pos, const std::exception& e); +// Logs unknown exception caught at 'pos'. +// Assumed to be called from catch (...) {} +void reportUnknownError(const SourceCodePos& pos); + +std::string makeMessage(const std::exception& e, const SourceCodePos& pos); + +std::string joinErrorMessages(const std::string& a, const std::string& b); + + +template +class JpError: public Base { +public: + JpError(const Base& e, const SourceCodePos& pos): + Base(e), msg(::makeMessage(e, pos)) { + } + + ~JpError() throw() { + } + + // override Base::what() + const char* what() const throw() { + return msg.c_str(); + } +private: + // Assert Base is derived from std::exception + enum { isDerivedFromStdException = + sizeof(static_cast((Base*)0)) }; + + std::string msg; +}; + +template +inline JpError makeException(const T& obj, const SourceCodePos& p) { + return JpError(obj, p); +} + +inline JpError makeException( + const std::string& msg, const SourceCodePos& p) { + return JpError(std::runtime_error(msg), p); +} + +inline JpError makeException( + const tstrings::any& msg, const SourceCodePos& p) { + return makeException(msg.str(), p); +} + +inline JpError makeException( + std::string::const_pointer msg, const SourceCodePos& p) { + return makeException(std::string(msg), p); +} + + +#define JP_REPORT_ERROR(e) reportError(JP_SOURCE_CODE_POS, e) +#define JP_REPORT_UNKNOWN_ERROR reportUnknownError(JP_SOURCE_CODE_POS) + +// Redefine locally in cpp file(s) if need more handling than just reporting +#define JP_HANDLE_ERROR(e) JP_REPORT_ERROR(e) +#define JP_HANDLE_UNKNOWN_ERROR JP_REPORT_UNKNOWN_ERROR + + +#define JP_TRY \ + try \ + { \ + do {} while(0) + +#define JP_DEFAULT_CATCH_EXCEPTIONS \ + JP_CATCH_STD_EXCEPTION \ + JP_CATCH_UNKNOWN_EXCEPTION + +#define JP_CATCH_EXCEPTIONS \ + JP_DEFAULT_CATCH_EXCEPTIONS + +#define JP_CATCH_ALL \ + } \ + JP_CATCH_EXCEPTIONS \ + do {} while(0) + +#define JP_CATCH_STD_EXCEPTION \ + catch (const std::exception& e) \ + { \ + JP_HANDLE_ERROR(e); \ + } + +#define JP_CATCH_UNKNOWN_EXCEPTION \ + catch (...) \ + { \ + JP_HANDLE_UNKNOWN_ERROR; \ + } + + +#define JP_THROW(e) throw makeException((e), JP_SOURCE_CODE_POS) + +#define JP_NO_THROW(expr) \ + JP_TRY; \ + expr; \ + JP_CATCH_ALL + +#endif // #ifndef ErrorHandling_h diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/FileUtils.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/FileUtils.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/FileUtils.cpp @@ -0,0 +1,709 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include +#include + +#include "FileUtils.h" +#include "WinErrorHandling.h" +#include "Log.h" + + +// Needed by FileUtils::isDirectoryNotEmpty +#pragma comment(lib, "shlwapi") + + +namespace FileUtils { + +namespace { + + +tstring reservedFilenameChars() { + tstring buf; + for (char charCode = 0; charCode < 32; ++charCode) { + buf.append(1, charCode); + } + buf += _T("<>:\"|?*/\\"); + return buf; +} + +} // namespace + +bool isDirSeparator(const tstring::value_type c) { + return (c == '/' || c == '\\'); +} + +bool isFileExists(const tstring &filePath) { + return GetFileAttributes(filePath.c_str()) != INVALID_FILE_ATTRIBUTES; +} + +namespace { +bool isDirectoryAttrs(const DWORD attrs) { + return attrs != INVALID_FILE_ATTRIBUTES + && (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0; +} +} // namespace + +bool isDirectory(const tstring &filePath) { + return isDirectoryAttrs(GetFileAttributes(filePath.c_str())); +} + +bool isDirectoryNotEmpty(const tstring &dirPath) { + if (!isDirectory(dirPath)) { + return false; + } + return FALSE == PathIsDirectoryEmpty(dirPath.c_str()); +} + +tstring dirname(const tstring &path) { + tstring::size_type pos = path.find_last_of(_T("\\/")); + if (pos != tstring::npos) { + pos = path.find_last_not_of(_T("\\/"), pos); // skip trailing slashes + } + return pos == tstring::npos ? tstring() : path.substr(0, pos + 1); +} + +tstring basename(const tstring &path) { + const tstring::size_type pos = path.find_last_of(_T("\\/")); + if (pos == tstring::npos) { + return path; + } + return path.substr(pos + 1); +} + +tstring suffix(const tstring &path) { + const tstring::size_type pos = path.rfind('.'); + if (pos == tstring::npos) { + return tstring(); + } + const tstring::size_type dirSepPos = path.find_first_of(_T("\\/"), + pos + 1); + if (dirSepPos != tstring::npos) { + return tstring(); + } + // test for '/..' and '..' cases + if (pos != 0 && path[pos - 1] == '.' + && (pos == 1 || isDirSeparator(path[pos - 2]))) { + return tstring(); + } + return path.substr(pos); +} + +tstring combinePath(const tstring& parent, const tstring& child) { + if (parent.empty()) { + return child; + } + if (child.empty()) { + return parent; + } + + tstring parentWOSlash = removeTrailingSlash(parent); + // also handle the case when child contains starting slash + bool childHasSlash = isDirSeparator(child.front()); + tstring childWOSlash = childHasSlash ? child.substr(1) : child; + + return parentWOSlash + _T("\\") + childWOSlash; +} + +tstring removeTrailingSlash(const tstring& path) { + if (path.empty()) { + return path; + } + tstring::const_reverse_iterator it = path.rbegin(); + tstring::const_reverse_iterator end = path.rend(); + + while (it != end && isDirSeparator(*it)) { + ++it; + } + return path.substr(0, end - it); +} + +tstring normalizePath(tstring v) { + std::replace(v.begin(), v.end(), '/', '\\'); + return tstrings::toLower(v); +} + +namespace { + +bool createNewFile(const tstring& path) { + HANDLE h = CreateFile(path.c_str(), GENERIC_WRITE, 0, NULL, CREATE_NEW, + FILE_ATTRIBUTE_NORMAL, NULL); + // if the file exists => h == INVALID_HANDLE_VALUE & GetLastError + // returns ERROR_FILE_EXISTS + if (h != INVALID_HANDLE_VALUE) { + CloseHandle(h); + LOG_TRACE(tstrings::any() << "Created [" << path << "] file"); + return true; + } + return false; +} + +} // namespace + +tstring createTempFile(const tstring &prefix, const tstring &suffix, + const tstring &path) { + const tstring invalidChars = reservedFilenameChars(); + + if (prefix.find_first_of(invalidChars) != tstring::npos) { + JP_THROW(tstrings::any() << "Illegal characters in prefix=" << prefix); + } + + if (suffix.find_first_of(invalidChars) != tstring::npos) { + JP_THROW(tstrings::any() << "Illegal characters in suffix=" << suffix); + } + + int rnd = (int)GetTickCount(); + + // do no more than 100 attempts + for (int i=0; i<100; i++) { + const tstring filePath = mkpath() << path << (prefix + + (tstrings::any() << (rnd + i)).tstr() + suffix); + if (createNewFile(filePath)) { + return filePath; + } + } + + // 100 attempts failed + JP_THROW(tstrings::any() << "createTempFile(" << prefix << ", " + << suffix << ", " + << path << ") failed"); +} + +tstring createTempDirectory(const tstring &prefix, const tstring &suffix, + const tstring &basedir) { + const tstring filePath = createTempFile(prefix, suffix, basedir); + // delete the file and create directory with the same name + deleteFile(filePath); + createDirectory(filePath); + return filePath; +} + +tstring createUniqueFile(const tstring &prototype) { + if (createNewFile(prototype)) { + return prototype; + } + + return createTempFile(replaceSuffix(basename(prototype)), + suffix(prototype), dirname(prototype)); +} + +namespace { + +void createDir(const tstring path, LPSECURITY_ATTRIBUTES saAttr, + tstring_array* createdDirs=0) { + if (CreateDirectory(path.c_str(), saAttr)) { + LOG_TRACE(tstrings::any() << "Created [" << path << "] directory"); + if (createdDirs) { + createdDirs->push_back(removeTrailingSlash(path)); + } + } else { + const DWORD createDirectoryErr = GetLastError(); + // if saAttr is specified, fail even if the directory exists + if (saAttr != NULL || !isDirectory(path)) { + JP_THROW(SysError(tstrings::any() << "CreateDirectory(" + << path << ") failed", CreateDirectory, createDirectoryErr)); + } + } +} + +} + +void createDirectory(const tstring &path, tstring_array* createdDirs) { + const tstring dirPath = removeTrailingSlash(path) + _T("\\"); + + tstring::size_type pos = dirPath.find_first_of(_T("\\/")); + while (pos != tstring::npos) { + const tstring subdirPath = dirPath.substr(0, pos + 1); + createDir(subdirPath, NULL, createdDirs); + pos = dirPath.find_first_of(_T("\\/"), pos + 1); + } +} + + +void copyFile(const tstring& fromPath, const tstring& toPath, + bool failIfExists) { + createDirectory(dirname(toPath)); + if (!CopyFile(fromPath.c_str(), toPath.c_str(), + (failIfExists ? TRUE : FALSE))) { + JP_THROW(SysError(tstrings::any() + << "CopyFile(" << fromPath << ", " << toPath << ", " + << failIfExists << ") failed", CopyFile)); + } + LOG_TRACE(tstrings::any() << "Copied [" << fromPath << "] file to [" + << toPath << "]"); +} + + +namespace { + +void moveFileImpl(const tstring& fromPath, const tstring& toPath, + DWORD flags) { + const bool isDir = isDirectory(fromPath); + if (!MoveFileEx(fromPath.c_str(), toPath.empty() ? NULL : toPath.c_str(), + flags)) { + JP_THROW(SysError(tstrings::any() << "MoveFileEx(" << fromPath + << ", " << toPath << ", " << flags << ") failed", MoveFileEx)); + } + + const bool onReboot = 0 != (flags & MOVEFILE_DELAY_UNTIL_REBOOT); + + const LPCTSTR label = isDir ? _T("folder") : _T("file"); + + tstrings::any msg; + if (!toPath.empty()) { + if (onReboot) { + msg << "Move"; + } else { + msg << "Moved"; + } + msg << " '" << fromPath << "' " << label << " to '" << toPath << "'"; + } else { + if (onReboot) { + msg << "Delete"; + } else { + msg << "Deleted"; + } + msg << " '" << fromPath << "' " << label; + } + if (onReboot) { + msg << " on reboot"; + } + LOG_TRACE(msg); +} + +} // namespace + + +void moveFile(const tstring& fromPath, const tstring& toPath, + bool failIfExists) { + createDirectory(dirname(toPath)); + + DWORD flags = MOVEFILE_COPY_ALLOWED; + if (!failIfExists) { + flags |= MOVEFILE_REPLACE_EXISTING; + } + + moveFileImpl(fromPath, toPath, flags); +} + +void deleteFile(const tstring &path) +{ + if (!deleteFile(path, std::nothrow)) { + JP_THROW(SysError(tstrings::any() + << "DeleteFile(" << path << ") failed", DeleteFile)); + } +} + +namespace { + +bool notFound(const DWORD status=GetLastError()) { + return status == ERROR_FILE_NOT_FOUND || status == ERROR_PATH_NOT_FOUND; +} + +bool deleteFileImpl(const std::nothrow_t &, const tstring &path) { + const bool deleted = (DeleteFile(path.c_str()) != 0); + if (deleted) { + LOG_TRACE(tstrings::any() << "Deleted [" << path << "] file"); + return true; + } + return notFound(); +} + +} // namespace + +bool deleteFile(const tstring &path, const std::nothrow_t &) throw() +{ + bool deleted = deleteFileImpl(std::nothrow, path); + const DWORD status = GetLastError(); + if (!deleted && status == ERROR_ACCESS_DENIED) { + DWORD attrs = GetFileAttributes(path.c_str()); + SetLastError(status); + if (attrs == INVALID_FILE_ATTRIBUTES) { + return false; + } + if (attrs & FILE_ATTRIBUTE_READONLY) { + // DeleteFile() failed because file is R/O. + // Remove R/O attribute and retry DeleteFile(). + attrs &= ~FILE_ATTRIBUTE_READONLY; + if (SetFileAttributes(path.c_str(), attrs)) { + LOG_TRACE(tstrings::any() << "Discarded R/O attribute from [" + << path << "] file"); + deleted = deleteFileImpl(std::nothrow, path); + } else { + LOG_WARNING(SysError(tstrings::any() + << "Failed to discard R/O attribute from [" + << path << "] file. File will not be deleted", + SetFileAttributes).what()); + SetLastError(status); + } + } + } + + return deleted || notFound(); +} + +void deleteDirectory(const tstring &path) +{ + if (!deleteDirectory(path, std::nothrow)) { + JP_THROW(SysError(tstrings::any() + << "RemoveDirectory(" << path << ") failed", RemoveDirectory)); + } +} + +bool deleteDirectory(const tstring &path, const std::nothrow_t &) throw() +{ + const bool deleted = (RemoveDirectory(path.c_str()) != 0); + if (deleted) { + LOG_TRACE(tstrings::any() << "Deleted [" << path << "] directory"); + } + return deleted || notFound(); +} + +namespace { + +class DeleteFilesCallback: public DirectoryCallback { +public: + explicit DeleteFilesCallback(bool ff): failfast(ff), failed(false) { + } + + virtual bool onFile(const tstring& path) { + if (failfast) { + deleteFile(path); + } else { + updateStatus(deleteFile(path, std::nothrow)); + } + return true; + } + + bool good() const { + return !failed; + } + +protected: + void updateStatus(bool success) { + if (!success) { + failed = true; + } + } + + const bool failfast; +private: + bool failed; +}; + +class DeleteAllCallback: public DeleteFilesCallback { +public: + explicit DeleteAllCallback(bool failfast): DeleteFilesCallback(failfast) { + } + + virtual bool onDirectory(const tstring& path) { + if (failfast) { + deleteDirectoryRecursive(path); + } else { + updateStatus(deleteDirectoryRecursive(path, std::nothrow)); + } + return true; + } +}; + + +class BatchDeleter { + const tstring dirPath; + bool recursive; +public: + explicit BatchDeleter(const tstring& path): dirPath(path) { + deleteSubdirs(false); + } + + BatchDeleter& deleteSubdirs(bool v) { + recursive = v; + return *this; + } + + void execute() const { + if (!isFileExists(dirPath)) { + return; + } + iterateDirectory(true /* fail fast */); + if (recursive) { + deleteDirectory(dirPath); + } + } + + bool execute(const std::nothrow_t&) const { + if (!isFileExists(dirPath)) { + return true; + } + + if (!isDirectory(dirPath)) { + return false; + } + + JP_TRY; + if (!iterateDirectory(false /* ignore errors */)) { + return false; + } + if (recursive) { + return deleteDirectory(dirPath, std::nothrow); + } + return true; + JP_CATCH_ALL; + + return false; + } + +private: + bool iterateDirectory(bool failfast) const { + std::unique_ptr callback; + if (recursive) { + callback = std::unique_ptr( + new DeleteAllCallback(failfast)); + } else { + callback = std::unique_ptr( + new DeleteFilesCallback(failfast)); + } + + FileUtils::iterateDirectory(dirPath, *callback); + return callback->good(); + } +}; + +} // namespace + +void deleteFilesInDirectory(const tstring &dirPath) { + BatchDeleter(dirPath).execute(); +} + +bool deleteFilesInDirectory(const tstring &dirPath, + const std::nothrow_t &) throw() { + return BatchDeleter(dirPath).execute(std::nothrow); +} + +void deleteDirectoryRecursive(const tstring &dirPath) { + BatchDeleter(dirPath).deleteSubdirs(true).execute(); +} + +bool deleteDirectoryRecursive(const tstring &dirPath, + const std::nothrow_t &) throw() { + return BatchDeleter(dirPath).deleteSubdirs(true).execute(std::nothrow); +} + +namespace { + +struct FindFileDeleter { + typedef HANDLE pointer; + + void operator()(HANDLE h) { + if (h && h != INVALID_HANDLE_VALUE) { + FindClose(h); + } + } +}; + +typedef std::unique_ptr UniqueFindFileHandle; + +}; // namesace +void iterateDirectory(const tstring &dirPath, DirectoryCallback& callback) +{ + const tstring searchString = combinePath(dirPath, _T("*")); + WIN32_FIND_DATA findData; + UniqueFindFileHandle h(FindFirstFile(searchString.c_str(), &findData)); + if (h.get() == INVALID_HANDLE_VALUE) { + // GetLastError() == ERROR_FILE_NOT_FOUND is OK + // - no files in the directory + // ERROR_PATH_NOT_FOUND is returned + // if the parent directory does not exist + if (GetLastError() != ERROR_FILE_NOT_FOUND) { + JP_THROW(SysError(tstrings::any() << "FindFirstFile(" + << dirPath << ") failed", FindFirstFile)); + } + return; + } + + do { + const tstring fname(findData.cFileName); + const tstring filePath = combinePath(dirPath, fname); + if (!isDirectoryAttrs(findData.dwFileAttributes)) { + if (!callback.onFile(filePath)) { + return; + } + } else if (fname != _T(".") && fname != _T("..")) { + if (!callback.onDirectory(filePath)) { + return; + } + } + } while (FindNextFile(h.get(), &findData)); + + // expect GetLastError() == ERROR_NO_MORE_FILES + if (GetLastError() != ERROR_NO_MORE_FILES) { + JP_THROW(SysError(tstrings::any() << "FindNextFile(" + << dirPath << ") failed", FindNextFile)); + } +} + + +tstring replaceSuffix(const tstring& path, const tstring& newSuffix) { + return (path.substr(0, path.size() - suffix(path).size()) + newSuffix); +} + + +DirectoryIterator& DirectoryIterator::findItems(tstring_array& v) { + if (!isDirectory(root)) { + return *this; + } + + iterateDirectory(root, *this); + v.insert(v.end(), items.begin(), items.end()); + items = tstring_array(); + return *this; +} + +bool DirectoryIterator::onFile(const tstring& path) { + if (theWithFiles) { + items.push_back(path); + } + return true; +} + +bool DirectoryIterator::onDirectory(const tstring& path) { + if (theWithFolders) { + items.push_back(path); + } + if (theRecurse) { + DirectoryIterator(path).recurse(theRecurse) + .withFiles(theWithFiles) + .withFolders(theWithFolders) + .findItems(items); + } + return true; +} + + +namespace { + +struct DeleterFunctor { + // Order of items in the following enum is important! + // It controls order in which items of particular type will be deleted. + // See Deleter::execute(). + enum { + File, + FilesInDirectory, + RecursiveDirectory, + EmptyDirectory + }; + + void operator () (const Deleter::Path& path) const { + switch (path.second) { +#define DELETE_SOME(o, f)\ + case o:\ + f(path.first, std::nothrow);\ + break + + DELETE_SOME(File, deleteFile); + DELETE_SOME(EmptyDirectory, deleteDirectory); + DELETE_SOME(FilesInDirectory, deleteFilesInDirectory); + DELETE_SOME(RecursiveDirectory, deleteDirectoryRecursive); + +#undef DELETE_SOME + default: + break; + } + } +}; + +} // namespace + +void Deleter::execute() { + Paths tmp; + tmp.swap(paths); + + // Reorder items to delete. + std::stable_sort(tmp.begin(), tmp.end(), [] (const Paths::value_type& a, + const Paths::value_type& b) { + return a.second < b.second; + }); + + std::for_each(tmp.begin(), tmp.end(), DeleterFunctor()); +} + +Deleter& Deleter::appendFile(const tstring& path) { + paths.push_back(std::make_pair(path, DeleterFunctor::File)); + return *this; +} + +Deleter& Deleter::appendEmptyDirectory(const Directory& dir) { + tstring path = normalizePath(removeTrailingSlash(dir)); + const tstring parent = normalizePath(removeTrailingSlash(dir.parent)); + while(parent != path) { + appendEmptyDirectory(path); + path = dirname(path); + } + + return *this; +} + +Deleter& Deleter::appendEmptyDirectory(const tstring& path) { + paths.push_back(std::make_pair(path, DeleterFunctor::EmptyDirectory)); + return *this; +} + +Deleter& Deleter::appendAllFilesInDirectory(const tstring& path) { + paths.push_back(std::make_pair(path, DeleterFunctor::FilesInDirectory)); + return *this; +} + +Deleter& Deleter::appendRecursiveDirectory(const tstring& path) { + paths.push_back(std::make_pair(path, DeleterFunctor::RecursiveDirectory)); + return *this; +} + + +FileWriter::FileWriter(const tstring& path): dstPath(path) { + tmpFile = FileUtils::createTempFile(_T("jds"), _T(".tmp"), + FileUtils::dirname(path)); + + cleaner.appendFile(tmpFile); + + // we want to get exception on error + tmp.exceptions(std::ifstream::failbit | std::ifstream::badbit); + tmp.open(tmpFile, std::ios::binary | std::ios::trunc); +} + +FileWriter& FileWriter::write(const void* buf, size_t bytes) { + tmp.write(static_cast(buf), bytes); + return *this; +} + +void FileWriter::finalize() { + tmp.close(); + + FileUtils::moveFile(tmpFile, dstPath, false); + + // cancel file deletion + cleaner.cancel(); +} + +} // namespace FileUtils diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/FileUtils.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/FileUtils.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/FileUtils.h @@ -0,0 +1,398 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef FILEUTILS_H +#define FILEUTILS_H + + +#include +#include "SysInfo.h" + + +namespace FileUtils { + + // Returns 'true' if the given character is a path separator. + bool isDirSeparator(const tstring::value_type c); + + // checks if the file or directory exists + bool isFileExists(const tstring &filePath); + + // checks is the specified file is a directory + // returns false if the path does not exist + bool isDirectory(const tstring &filePath); + + // checks if the specified directory is not empty + // returns true if the path is an existing directory and + // it contains at least one file other than "." or "..". + bool isDirectoryNotEmpty(const tstring &dirPath); + + // returns directory part of the path. + // returns empty string if the path contains only filename. + // if the path ends with slash/backslash, + // returns removeTrailingSlashes(path). + tstring dirname(const tstring &path); + + // returns basename part of the path + // if the path ends with slash/backslash, returns empty string. + tstring basename(const tstring &path); + + /** + * Translates forward slashes to back slashes and returns lower case version + * of the given string. + */ + tstring normalizePath(tstring v); + + // Returns suffix of the path. If the given path has a suffix the first + // character of the return value is '.'. + // Otherwise return value if empty string. + tstring suffix(const tstring &path); + + // combines two strings into a path + tstring combinePath(const tstring& parent, const tstring& child); + + // removes trailing slashes and backslashes in the path if any + tstring removeTrailingSlash(const tstring& path); + + // Creates a file with unique name in the specified base directory, + // throws an exception if operation fails + // path is constructed as . + // The function fails and throws exception if 'path' doesn't exist. + tstring createTempFile(const tstring &prefix = _T(""), + const tstring &suffix = _T(".tmp"), + const tstring &path=SysInfo::getTempDir()); + + // Creates a directory with unique name in the specified base directory, + // throws an exception if operation fails + // path is constructed as + // The function fails and throws exception if 'path' doesn't exist. + tstring createTempDirectory(const tstring &prefix = _T(""), + const tstring &suffix = _T(".tmp"), + const tstring &basedir=SysInfo::getTempDir()); + + // If the file referenced with "prototype" parameter DOES NOT exist, + // the return value is the given path. No new files created. + // Otherwise the function creates another file in the same directory as + // the given file with the same suffix and with the basename from the + // basename of the given file with some random chars appended to ensure + // created file is unique. + tstring createUniqueFile(const tstring &prototype); + + // Creates directory and subdirectories if don't exist. + // Currently supports only "standard" path like "c:\bla-bla" + // If 'createdDirs' parameter is not NULL, the given array is appended with + // all subdirectories created by this function call. + void createDirectory(const tstring &path, tstring_array* createdDirs=0); + + // copies file from fromPath to toPath. + // Creates output directory if doesn't exist. + void copyFile(const tstring& fromPath, const tstring& toPath, + bool failIfExists); + + // moves file from fromPath to toPath. + // Creates output directory if doesn't exist. + void moveFile(const tstring& fromPath, const tstring& toPath, + bool failIfExists); + + // Throws exception if fails to delete specified 'path'. + // Exits normally if 'path' doesn't exist or it has been deleted. + // Attempts to strip R/O attribute if delete fails and retry delete. + void deleteFile(const tstring &path); + // Returns 'false' if fails to delete specified 'path'. + // Returns 'true' if 'path' doesn't exist or it has been deleted. + // Attempts to strip R/O attribute if delete fails and retry delete. + bool deleteFile(const tstring &path, const std::nothrow_t &) throw(); + + // Like deleteFile(), but applies to directories. + void deleteDirectory(const tstring &path); + bool deleteDirectory(const tstring &path, const std::nothrow_t &) throw(); + + // Deletes all files (not subdirectories) from the specified directory. + // Exits normally if all files in 'dirPath' have been deleted or if + // 'dirPath' doesn't exist. + // Throws exception if 'dirPath' references existing file system object + // which is not a directory or when the first failure of file delete + // occurs. + void deleteFilesInDirectory(const tstring &dirPath); + // Deletes all files (not subdirectories) from the specified directory. + // Returns 'true' normally if all files in 'dirPath' have been deleted or + // if 'dirPath' doesn't exist. + // Returns 'false' if 'dirPath' references existing file system object + // which is not a directory or if failed to delete one ore more files in + // 'dirPath' directory. + // Doesn't abort iteration over files if the given directory after the + // first failure to delete a file. + bool deleteFilesInDirectory(const tstring &dirPath, + const std::nothrow_t &) throw(); + // Like deleteFilesInDirectory, but deletes subdirectories as well + void deleteDirectoryRecursive(const tstring &dirPath); + bool deleteDirectoryRecursive(const tstring &dirPath, + const std::nothrow_t &) throw(); + + class DirectoryCallback { + public: + virtual ~DirectoryCallback() {}; + + virtual bool onFile(const tstring& path) { + return true; + } + virtual bool onDirectory(const tstring& path) { + return true; + } + }; + + // Calls the given callback for every file and subdirectory of + // the given directory. + void iterateDirectory(const tstring &dirPath, DirectoryCallback& callback); + + /** + * Replace file suffix, example replaceSuffix("file/path.txt", ".csv") + * @param path file path to replace suffix + * @param suffix new suffix for path + * @return return file path with new suffix + */ + tstring replaceSuffix(const tstring& path, const tstring& suffix=tstring()); + + class DirectoryIterator: DirectoryCallback { + public: + DirectoryIterator(const tstring& root=tstring()): root(root) { + recurse().withFiles().withFolders(); + } + + DirectoryIterator& recurse(bool v=true) { + theRecurse = v; + return *this; + } + + DirectoryIterator& withFiles(bool v=true) { + theWithFiles = v; + return *this; + } + + DirectoryIterator& withFolders(bool v=true) { + theWithFolders = v; + return *this; + } + + tstring_array findItems() { + tstring_array reply; + findItems(reply); + return reply; + } + + DirectoryIterator& findItems(tstring_array& v); + + private: + virtual bool onFile(const tstring& path); + virtual bool onDirectory(const tstring& path); + + private: + bool theRecurse; + bool theWithFiles; + bool theWithFolders; + tstring root; + tstring_array items; + }; + + // Returns array of all the files/sub-folders from the given directory, + // empty array if basedir is not a directory. The returned + // array is ordered from top down (i.e. dirs are listed first followed + // by subfolders and files). + // Order of subfolders and files is undefined + // but usually they are sorted by names. + inline tstring_array listAllContents(const tstring& basedir) { + return DirectoryIterator(basedir).findItems(); + } + + // Helper to construct path from multiple components. + // + // Sample usage: + // Construct "c:\Program Files\Java" string from three components + // + // tstring path = FileUtils::mkpath() << _T("c:") + // << _T("Program Files") + // << _T("Java"); + // + class mkpath { + public: + operator const tstring& () const { + return path; + } + + mkpath& operator << (const tstring& p) { + path = combinePath(path, p); + return *this; + } + + // mimic std::string + const tstring::value_type* c_str() const { + return path.c_str(); + } + private: + tstring path; + }; + + struct Directory { + Directory() { + } + + Directory(const tstring &parent, + const tstring &subdir) : parent(parent), subdir(subdir) { + } + + operator tstring () const { + return getPath(); + } + + tstring getPath() const { + return combinePath(parent, subdir); + } + + bool empty() const { + return (parent.empty() && subdir.empty()); + } + + tstring parent; + tstring subdir; + }; + + // Deletes list of files and directories in batch mode. + // Registered files and directories are deleted when destructor is called. + // Order or delete operations is following: + // - delete items registered with appendFile() calls; + // - delete items registered with appendAllFilesInDirectory() calls; + // - delete items registered with appendRecursiveDirectory() calls; + // - delete items registered with appendEmptyDirectory() calls. + class Deleter { + public: + Deleter() { + } + + ~Deleter() { + execute(); + } + + typedef std::pair Path; + typedef std::vector Paths; + + /** + * Appends all records from the given deleter Deleter into this Deleter + * instance. On success array with records in the passed in Deleter + * instance is emptied. + */ + Deleter& appendFrom(Deleter& other) { + Paths tmp(paths); + tmp.insert(tmp.end(), other.paths.begin(), other.paths.end()); + Paths empty; + other.paths.swap(empty); + paths.swap(tmp); + return *this; + } + + // Schedule file for deletion. + Deleter& appendFile(const tstring& path); + + // Schedule files for deletion. + template + Deleter& appendFiles(It b, It e) { + for (It it = b; it != e; ++it) { + appendFile(*it); + } + return *this; + } + + // Schedule files for deletion in the given directory. + template + Deleter& appendFiles(const tstring& dirname, It b, It e) { + for (It it = b; it != e; ++it) { + appendFile(FileUtils::mkpath() << dirname << *it); + } + return *this; + } + + // Schedule empty directory for deletion with empty roots + // (up to Directory.parent). + Deleter& appendEmptyDirectory(const Directory& dir); + + // Schedule empty directory for deletion without roots. + // This is a particular case of + // appendEmptyDirectory(const Directory& dir) + // with Directory(dirname(path), basename(path)). + Deleter& appendEmptyDirectory(const tstring& path); + + // Schedule all file from the given directory for deletion. + Deleter& appendAllFilesInDirectory(const tstring& path); + + // Schedule directory for recursive deletion. + Deleter& appendRecursiveDirectory(const tstring& path); + + void cancel() { + paths.clear(); + } + + // Deletes scheduled files and directories. After this function + // is called internal list of scheduled items is emptied. + void execute(); + + private: + Paths paths; + }; + + + /** + * Helper to write chunks of data into binary file. + * Creates temporary file in the same folder with destination file. + * All subsequent requests to save data chunks are redirected to temporary + * file. finalize() method closes temporary file stream and renames + * temporary file. + * If finalize() method is not called, temporary file is deleted in + * ~FileWriter(), destination file is not touched. + */ + class FileWriter { + public: + explicit FileWriter(const tstring& path); + + FileWriter& write(const void* buf, size_t bytes); + + template + FileWriter& write(const Ctnr& buf) { + return write(buf.data(), + buf.size() * sizeof(typename Ctnr::value_type)); + } + + void finalize(); + + private: + // Not accessible by design! + FileWriter& write(const std::wstring& str); + + private: + tstring tmpFile; + Deleter cleaner; + std::ofstream tmp; + tstring dstPath; + }; +} // FileUtils + +#endif // FILEUTILS_H diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/IconSwap.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/IconSwap.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/IconSwap.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include +#include +#include +#include + +using namespace std; + +// http://msdn.microsoft.com/en-us/library/ms997538.aspx + +typedef struct _ICONDIRENTRY { + BYTE bWidth; + BYTE bHeight; + BYTE bColorCount; + BYTE bReserved; + WORD wPlanes; + WORD wBitCount; + DWORD dwBytesInRes; + DWORD dwImageOffset; +} ICONDIRENTRY, * LPICONDIRENTRY; + +typedef struct _ICONDIR { + WORD idReserved; + WORD idType; + WORD idCount; + ICONDIRENTRY idEntries[1]; +} ICONDIR, * LPICONDIR; + +// #pragmas are used here to insure that the structure's +// packing in memory matches the packing of the EXE or DLL. +#pragma pack(push) +#pragma pack(2) + +typedef struct _GRPICONDIRENTRY { + BYTE bWidth; + BYTE bHeight; + BYTE bColorCount; + BYTE bReserved; + WORD wPlanes; + WORD wBitCount; + DWORD dwBytesInRes; + WORD nID; +} GRPICONDIRENTRY, * LPGRPICONDIRENTRY; +#pragma pack(pop) + +#pragma pack(push) +#pragma pack(2) + +typedef struct _GRPICONDIR { + WORD idReserved; + WORD idType; + WORD idCount; + GRPICONDIRENTRY idEntries[1]; +} GRPICONDIR, * LPGRPICONDIR; +#pragma pack(pop) + +void PrintError() { + LPVOID message = NULL; + DWORD error = GetLastError(); + + if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER + | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, error, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR) & message, 0, NULL) != 0) { + printf("%S", (LPTSTR) message); + LocalFree(message); + } +} + +// Note: We do not check here that iconTarget is valid icon. +// Java code will already do this for us. + +bool ChangeIcon(wstring iconTarget, wstring launcher) { + WORD language = MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT); + + HANDLE icon = CreateFile(iconTarget.c_str(), GENERIC_READ, 0, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (icon == INVALID_HANDLE_VALUE) { + PrintError(); + return false; + } + + // Reading .ICO file + WORD idReserved, idType, idCount; + + DWORD dwBytesRead; + ReadFile(icon, &idReserved, sizeof (WORD), &dwBytesRead, NULL); + ReadFile(icon, &idType, sizeof (WORD), &dwBytesRead, NULL); + ReadFile(icon, &idCount, sizeof (WORD), &dwBytesRead, NULL); + + LPICONDIR lpid = (LPICONDIR) malloc( + sizeof (ICONDIR) + (sizeof (ICONDIRENTRY) * (idCount - 1))); + if (lpid == NULL) { + CloseHandle(icon); + printf("Error: Failed to allocate memory\n"); + return false; + } + + lpid->idReserved = idReserved; + lpid->idType = idType; + lpid->idCount = idCount; + + ReadFile(icon, &lpid->idEntries[0], sizeof (ICONDIRENTRY) * lpid->idCount, + &dwBytesRead, NULL); + + LPGRPICONDIR lpgid = (LPGRPICONDIR) malloc( + sizeof (GRPICONDIR) + (sizeof (GRPICONDIRENTRY) * (idCount - 1))); + if (lpid == NULL) { + CloseHandle(icon); + free(lpid); + printf("Error: Failed to allocate memory\n"); + return false; + } + + lpgid->idReserved = idReserved; + lpgid->idType = idType; + lpgid->idCount = idCount; + + for (int i = 0; i < lpgid->idCount; i++) { + lpgid->idEntries[i].bWidth = lpid->idEntries[i].bWidth; + lpgid->idEntries[i].bHeight = lpid->idEntries[i].bHeight; + lpgid->idEntries[i].bColorCount = lpid->idEntries[i].bColorCount; + lpgid->idEntries[i].bReserved = lpid->idEntries[i].bReserved; + lpgid->idEntries[i].wPlanes = lpid->idEntries[i].wPlanes; + lpgid->idEntries[i].wBitCount = lpid->idEntries[i].wBitCount; + lpgid->idEntries[i].dwBytesInRes = lpid->idEntries[i].dwBytesInRes; + lpgid->idEntries[i].nID = i + 1; + } + + // Store images in .EXE + HANDLE update = BeginUpdateResource(launcher.c_str(), FALSE); + if (update == NULL) { + free(lpid); + free(lpgid); + CloseHandle(icon); + PrintError(); + return false; + } + + for (int i = 0; i < lpid->idCount; i++) { + LPBYTE lpBuffer = (LPBYTE) malloc(lpid->idEntries[i].dwBytesInRes); + SetFilePointer(icon, lpid->idEntries[i].dwImageOffset, + NULL, FILE_BEGIN); + ReadFile(icon, lpBuffer, lpid->idEntries[i].dwBytesInRes, + &dwBytesRead, NULL); + if (!UpdateResource(update, RT_ICON, + MAKEINTRESOURCE(lpgid->idEntries[i].nID), + language, &lpBuffer[0], lpid->idEntries[i].dwBytesInRes)) { + free(lpBuffer); + free(lpid); + free(lpgid); + CloseHandle(icon); + PrintError(); + return false; + } + free(lpBuffer); + } + + free(lpid); + CloseHandle(icon); + + if (!UpdateResource(update, RT_GROUP_ICON, MAKEINTRESOURCE(1), + language, &lpgid[0], (sizeof (WORD) * 3) + + (sizeof (GRPICONDIRENTRY) * lpgid->idCount))) { + free(lpgid); + PrintError(); + return false; + } + + free(lpgid); + + if (EndUpdateResource(update, FALSE) == FALSE) { + PrintError(); + return false; + } + + return true; +} diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/IconSwap.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/IconSwap.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/IconSwap.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef ICONSWAP_H +#define ICONSWAP_H + +#include + +using namespace std; + +bool ChangeIcon(wstring iconTarget, wstring launcher); + +#endif // ICONSWAP_H + diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/Log.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/Log.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/Log.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Log.h" +#include "SysInfo.h" +#include "FileUtils.h" + + +namespace { + // + // IMPORTANT: Static objects with non-trivial constructors are NOT allowed + // in logger module. Allocate buffers only and do lazy initialization of + // globals in Logger::getDefault(). + // + // Logging subsystem is used almost in every module, and logging API can be + // called from constructors of static objects in various modules. As + // ordering of static objects initialization between modules is undefined, + // this means some module may call logging api before logging static + // variables are initialized if any. This will result in AV. To avoid such + // use cases keep logging module free from static variables that require + // initialization with functions called by CRT. + // + + // by default log everything + const Logger::LogLevel defaultLogLevel = Logger::LOG_TRACE; + + char defaultLogAppenderMemory[sizeof(StderrLogAppender)] = {}; + + char defaultLoggerMemory[sizeof(Logger)] = {}; + + NopLogAppender nopLogApender; + + LPCTSTR getLogLevelStr(Logger::LogLevel level) { + switch (level) { + case Logger::LOG_TRACE: + return _T("TRACE"); + case Logger::LOG_INFO: + return _T("INFO"); + case Logger::LOG_WARNING: + return _T("WARNING"); + case Logger::LOG_ERROR: + return _T("ERROR"); + } + return _T("UNKNOWN"); + } + + tstring retrieveModuleName() { + try { + return FileUtils::basename(SysInfo::getCurrentModulePath()); + } catch (const std::exception&) { + return _T("Unknown"); + } + } + + TCHAR moduleName[MAX_PATH] = { 'U', 'n', 'k', 'o', 'w', 'n', TCHAR(0) }; + + const LPCTSTR format = _T("[%04u/%02u/%02u %02u:%02u:%02u.%03u, %s (PID: %u, TID: %u), %s:%u (%s)]\n\t%s: %s\n"); + + enum State { NotInitialized, Initializing, Initialized }; + State state = NotInitialized; +} + + +LogEvent::LogEvent() { + memset(this, 0, sizeof(*this)); + moduleName = tstring(); + logLevel = tstring(); + fileName = tstring(); + funcName = tstring(); + message = tstring(); +} + + +StderrLogAppender::StderrLogAppender() { +} + + +/*static*/ +Logger& Logger::defaultLogger() { + Logger* reply = reinterpret_cast(defaultLoggerMemory); + + if (!reply->appender) { + // Memory leak by design. Not an issue at all as this is global + // object. OS will do resources clean up anyways when application + // terminates and the default log appender should live as long as + // application lives. + reply->appender = new (defaultLogAppenderMemory) StderrLogAppender(); + } + + if (Initializing == state) { + // Recursive call to Logger::defaultLogger. + moduleName[0] = TCHAR(0); + } else if (NotInitialized == state) { + state = Initializing; + + tstring mname = retrieveModuleName(); + mname.resize(_countof(moduleName) - 1); + std::memcpy(moduleName, mname.c_str(), mname.size()); + moduleName[mname.size()] = TCHAR(0); + + // if JPACKAGE_DEBUG environment variable is NOT set to "true" disable + // logging. + if (SysInfo::getEnvVariable(std::nothrow, + L"JPACKAGE_DEBUG") != L"true") { + reply->appender = &nopLogApender; + } + + state = Initialized; + } + + return *reply; +} + +Logger::Logger(LogAppender& appender, LogLevel logLevel) + : level(logLevel), appender(&appender) { +} + +void Logger::setLogLevel(LogLevel logLevel) { + level = logLevel; +} + +Logger::~Logger() { +} + + +bool Logger::isLoggable(LogLevel logLevel) const { + return logLevel >= level; +} + +void Logger::log(LogLevel logLevel, LPCTSTR fileName, int lineNum, + LPCTSTR funcName, const tstring& message) const { + LogEvent logEvent; + + // [YYYY/MM/DD HH:MM:SS.ms, (PID: processID, TID: threadID), + // fileName:lineNum (funcName)] LEVEL: message + GetLocalTime(&logEvent.ts); + + logEvent.pid = GetCurrentProcessId(); + logEvent.tid = GetCurrentThreadId(); + logEvent.moduleName = moduleName; + logEvent.fileName = FileUtils::basename(fileName); + logEvent.funcName = funcName; + logEvent.logLevel = getLogLevelStr(logLevel); + logEvent.lineNum = lineNum; + logEvent.message = message; + + appender->append(logEvent); +} + + +void StderrLogAppender::append(const LogEvent& v) +{ + const tstring out = tstrings::unsafe_format(format, + unsigned(v.ts.wYear), unsigned(v.ts.wMonth), unsigned(v.ts.wDay), + unsigned(v.ts.wHour), unsigned(v.ts.wMinute), unsigned(v.ts.wSecond), + unsigned(v.ts.wMilliseconds), + v.moduleName.c_str(), v.pid, v.tid, + v.fileName.c_str(), v.lineNum, v.funcName.c_str(), + v.logLevel.c_str(), + v.message.c_str()); + + std::cerr << tstrings::toUtf8(out); +} + + +// Logger::ScopeTracer +Logger::ScopeTracer::ScopeTracer(Logger &logger, LogLevel logLevel, + LPCTSTR fileName, int lineNum, LPCTSTR funcName, + const tstring& scopeName) : log(logger), level(logLevel), + file(fileName), line(lineNum), + func(funcName), scope(scopeName), needLog(logger.isLoggable(logLevel)) { + if (needLog) { + log.log(level, file.c_str(), line, func.c_str(), + tstrings::any() << "Entering " << scope); + } +} + +Logger::ScopeTracer::~ScopeTracer() { + if (needLog) { + // we don't know what line is end of scope at, so specify line 0 + // and add note about line when the scope begins + log.log(level, file.c_str(), 0, func.c_str(), + tstrings::any() << "Exiting " << scope << " (entered at " + << FileUtils::basename(file) << ":" << line << ")"); + } +} diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/Log.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/Log.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/Log.h @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef __LOG_H_INCLUDED_ +#define __LOG_H_INCLUDED_ + +#include +#include "tstrings.h" + + +/* Default logger (Logger::defaultLogger()) writes log messages to + * the default log file. + * Common scenario: + * - main() function configures default logger: + * FileLogAppender appender(_T("my_log_filename.log")); + * Logger::defaultLogger().setAppender(appender); + * Logger::defaultLogger().setLogLevel(LOG_INFO); + * If the default file name and log level are not set, + * _T("jusched.log")/LOG_TRACE are used. + * + * Logger fileName specifies only file name, + * full path for the log file depends on the platform + * (usually value of the TMP env. var) + */ + +struct LogEvent { + SYSTEMTIME ts; + long tid; + long pid; + tstring moduleName; + tstring logLevel; + tstring fileName; + int lineNum; + tstring funcName; + tstring message; + + LogEvent(); +}; + + +class LogAppender { +public: + virtual ~LogAppender() { + } + virtual void append(const LogEvent& v) = 0; +}; + + +class NopLogAppender: public LogAppender { +public: + virtual void append(const LogEvent& v) {}; +}; + + +class TeeLogAppender: public LogAppender { +public: + TeeLogAppender(LogAppender* first, LogAppender* second): + first(first), second(second) { + } + virtual ~TeeLogAppender() { + } + virtual void append(const LogEvent& v) { + if (first) { + first->append(v); + } + if (second) { + second->append(v); + } + } +private: + LogAppender* first; + LogAppender* second; +}; + + +/** + * Writes log events to stderr. + */ +class StderrLogAppender: public LogAppender { +public: + explicit StderrLogAppender(); + + virtual void append(const LogEvent& v); +}; + + +class Logger { +public: + enum LogLevel { + LOG_TRACE, + LOG_INFO, + LOG_WARNING, + LOG_ERROR + }; + + static Logger& defaultLogger(); + + explicit Logger(LogAppender& appender, LogLevel logLevel = LOG_TRACE); + ~Logger(); + + LogAppender& setAppender(LogAppender& v) { + LogAppender& oldAppender = *appender; + appender = &v; + return oldAppender; + } + + LogAppender& getAppender() const { + return *appender; + } + + void setLogLevel(LogLevel logLevel); + + bool isLoggable(LogLevel logLevel) const ; + void log(LogLevel logLevel, LPCTSTR fileName, int lineNum, + LPCTSTR funcName, const tstring& message) const; + void log(LogLevel logLevel, LPCTSTR fileName, int lineNum, + LPCTSTR funcName, const tstrings::any& message) const { + return log(logLevel, fileName, lineNum, funcName, message.tstr()); + } + void log(LogLevel logLevel, LPCTSTR fileName, int lineNum, + LPCTSTR funcName, tstring::const_pointer message) const { + return log(logLevel, fileName, lineNum, funcName, tstring(message)); + } + + // internal class for scope tracing + class ScopeTracer { + public: + ScopeTracer(Logger &logger, LogLevel logLevel, LPCTSTR fileName, + int lineNum, LPCTSTR funcName, const tstring& scopeName); + ~ScopeTracer(); + + private: + const Logger &log; + const LogLevel level; + const bool needLog; + const tstring file; + const int line; + const tstring func; + const tstring scope; + }; + +private: + LogLevel level; + LogAppender* appender; +}; + + +// base logging macro +#define LOGGER_LOG(logger, logLevel, message) \ + do { \ + if (logger.isLoggable(logLevel)) { \ + logger.log(logLevel, _T(__FILE__), __LINE__, _T(__FUNCTION__), message); \ + } \ + } while(false) + + +// custom logger macros +#define LOGGER_TRACE(logger, message) LOGGER_LOG(logger, Logger::LOG_TRACE, message) +#define LOGGER_INFO(logger, message) LOGGER_LOG(logger, Logger::LOG_INFO, message) +#define LOGGER_WARNING(logger, message) LOGGER_LOG(logger, Logger::LOG_WARNING, message) +#define LOGGER_ERROR(logger, message) LOGGER_LOG(logger, Logger::LOG_ERROR, message) +// scope tracing macros +#define LOGGER_TRACE_SCOPE(logger, scopeName) \ + Logger::ScopeTracer tracer__COUNTER__(logger, Logger::LOG_TRACE, _T(__FILE__), __LINE__, _T(__FUNCTION__), scopeName) +#define LOGGER_TRACE_FUNCTION(logger) LOGGER_TRACE_SCOPE(logger, _T(__FUNCTION__)) + + +// default logger macros +#define LOG_TRACE(message) LOGGER_LOG(Logger::defaultLogger(), Logger::LOG_TRACE, message) +#define LOG_INFO(message) LOGGER_LOG(Logger::defaultLogger(), Logger::LOG_INFO, message) +#define LOG_WARNING(message) LOGGER_LOG(Logger::defaultLogger(), Logger::LOG_WARNING, message) +#define LOG_ERROR(message) LOGGER_LOG(Logger::defaultLogger(), Logger::LOG_ERROR, message) +// scope tracing macros +// logs (_T("Entering ") + scopeName) at the beging, (_T("Exiting ") + scopeName) at the end of scope +#define LOG_TRACE_SCOPE(scopeName) LOGGER_TRACE_SCOPE(Logger::defaultLogger(), scopeName) +// logs (_T("Entering ") + functionName) at the beging, (_T("Exiting ") + __FUNCTION__) at the end of scope +#define LOG_TRACE_FUNCTION() LOGGER_TRACE_FUNCTION(Logger::defaultLogger()) + + +#endif // __LOG_H_INCLUDED_ diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/ResourceEditor.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/ResourceEditor.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/ResourceEditor.cpp @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include +#include "ResourceEditor.h" +#include "WinErrorHandling.h" +#include "Log.h" + + +ResourceEditor::FileLock::FileLock(const std::wstring& binaryPath) { + h = BeginUpdateResource(binaryPath.c_str(), FALSE); + if (NULL == h) { + JP_THROW(SysError(tstrings::any() << "BeginUpdateResource(" + << binaryPath << ") failed", BeginUpdateResource)); + } + + discard(false); +} + + +ResourceEditor::FileLock::~FileLock() { + if (!EndUpdateResource(h, theDiscard)) { + JP_NO_THROW(JP_THROW(SysError(tstrings::any() + << "EndUpdateResource(" << h << ") failed.", EndUpdateResource))); + } +} + + +ResourceEditor::ResourceEditor() { + language(MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL)).type(unsigned(0)).id(unsigned(0)); +} + + +ResourceEditor& ResourceEditor::type(unsigned v) { + return type(MAKEINTRESOURCE(v)); +} + + +ResourceEditor& ResourceEditor::type(LPCWSTR v) { + if (IS_INTRESOURCE(v)) { + std::wostringstream printer; + printer << L"#" << reinterpret_cast(v); + theType = printer.str(); + theTypePtr = MAKEINTRESOURCE(static_cast(reinterpret_cast(v))); + } else { + theType = v; + theTypePtr = theType.c_str(); + } + return *this; +} + + +ResourceEditor& ResourceEditor::id(unsigned v) { + return id(MAKEINTRESOURCE(v)); +} + + +ResourceEditor& ResourceEditor::id(LPCWSTR v) { + if (IS_INTRESOURCE(v)) { + std::wostringstream printer; + printer << L"#" << reinterpret_cast(v); + theId = printer.str(); + } else { + theId = v; + theIdPtr = theId.c_str(); + } + return *this; +} + + +ResourceEditor& ResourceEditor::apply(const FileLock& dstBinary, + std::istream& srcStream, std::streamsize size) { + + typedef std::vector ByteArray; + ByteArray buf; + if (size <= 0) { + // Read the entire stream. + buf = ByteArray((std::istreambuf_iterator(srcStream)), + std::istreambuf_iterator()); + } else { + buf.resize(size_t(size)); + srcStream.read(reinterpret_cast(buf.data()), size); + } + + auto reply = UpdateResource(dstBinary.get(), theTypePtr, theIdPtr, lang, + buf.data(), static_cast(buf.size())); + if (reply == FALSE) { + JP_THROW(SysError("UpdateResource() failed", UpdateResource)); + } + + return *this; +} + + +ResourceEditor& ResourceEditor::apply(const FileLock& dstBinary, + const std::wstring& srcFile) { + std::ifstream input(srcFile, std::ios_base::binary); + input.exceptions(std::ios::failbit | std::ios::badbit); + return apply(dstBinary, input); +} diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/ResourceEditor.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/ResourceEditor.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/ResourceEditor.h @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef RESOURCEEDITOR_H +#define RESOURCEEDITOR_H + +#include +#include +#include + + +class ResourceEditor { +public: + class FileLock { + public: + FileLock(const std::wstring& binaryPath); + ~FileLock(); + + HANDLE get() const { + return h; + } + + void discard(bool v = true) { + theDiscard = v; + } + + private: + FileLock(const FileLock&); + FileLock& operator=(const FileLock&); + private: + HANDLE h; + bool theDiscard; + }; + +public: + ResourceEditor(); + + /** + * Set the language identifier of the resource to be updated. + */ + ResourceEditor& language(unsigned v) { + lang = v; + return *this; + } + + /** + * Set the resource type to be updated. + */ + ResourceEditor& type(unsigned v); + + /** + * Set the resource type to be updated. + */ + ResourceEditor& type(LPCWSTR v); + + /** + * Set resource ID. + */ + ResourceEditor& id(unsigned v); + + /** + * Set resource ID. + */ + ResourceEditor& id(LPCWSTR v); + + /** + * Relaces resource configured in the given binary with the given data stream. + */ + ResourceEditor& apply(const FileLock& dstBinary, std::istream& srcStream, std::streamsize size=0); + + /** + * Relaces resource configured in the given binary with contents of + * the given binary file. + */ + ResourceEditor& apply(const FileLock& dstBinary, const std::wstring& srcFile); + +private: + unsigned lang; + std::wstring theId; + LPCWSTR theIdPtr; + std::wstring theType; + LPCWSTR theTypePtr; +}; + +#endif // #ifndef RESOURCEEDITOR_H diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/SourceCodePos.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/SourceCodePos.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/SourceCodePos.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +#ifndef SourceCodePos_h +#define SourceCodePos_h + + +// +// Position in source code. +// + +struct SourceCodePos +{ + SourceCodePos(const char* fl, const char* fnc, int l): + file(fl), func(fnc), lno(l) + { + } + + const char* file; + const char* func; + int lno; +}; + + +// Initializes SourceCodePos instance with the +// information from the point of calling. +#define JP_SOURCE_CODE_POS SourceCodePos(__FILE__, __FUNCTION__, __LINE__) + + +#endif // #ifndef SourceCodePos_h diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/SysInfo.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/SysInfo.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/SysInfo.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +#ifndef SYSINFO_H +#define SYSINFO_H + +#include "tstrings.h" + + +// +// This namespace provides information about environment in which +// the current application runs. +// It is for general purpose use. +// Functions in this namespaces are just queries about the environment. +// Functions that change the existing environment like file or directory +// creation should not be added to this namespace. +// +namespace SysInfo { + /** + * Returns temp dir (for the current user). + */ + tstring getTempDir(); + + /** + * Returns absolute path to the process executable. + */ + tstring getProcessModulePath(); + + /** + * Returns absolute path to the current executable module. + */ + tstring getCurrentModulePath(); + + enum CommandArgProgramNameMode { + IncludeProgramName, + ExcludeProgramName + }; + /** + * Retrieves the command-line arguments for the current process. + * With IncludeProgramName option returns result similar to argv/argc. + * With ExcludeProgramName option program name + * (the 1st element of command line) + * is excluded. + */ + tstring_array getCommandArgs( + CommandArgProgramNameMode progNameMode = ExcludeProgramName); + + /** + * Returns value of environment variable with the given name. + * Throws exception if variable is not set or any other error occurred + * reading the value. + */ + tstring getEnvVariable(const tstring& name); + + /** + * Returns value of environment variable with the given name. + * Returns value of 'defValue' parameter if variable is not set or any + * other error occurred reading the value. + */ + tstring getEnvVariable(const std::nothrow_t&, const tstring& name, + const tstring& defValue=tstring()); + + /** + * Returns 'true' if environment variable with the given name is set. + */ + bool isEnvVariableSet(const tstring& name); +} + +#endif // SYSINFO_H diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/UniqueHandle.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/UniqueHandle.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/UniqueHandle.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef UNIQUEHANDLE_H +#define UNIQUEHANDLE_H + +#include +#include + + +struct WndHandleDeleter { + typedef HANDLE pointer; + + void operator()(HANDLE h) { + ::CloseHandle(h); + } +}; + +typedef std::unique_ptr UniqueHandle; + +#endif // #ifndef UNIQUEHANDLE_H diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/Utils.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/Utils.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/Utils.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Windows.h" +#include "Utils.h" + +#define BUFFER_SIZE 4096 + +wstring GetStringFromJString(JNIEnv *pEnv, jstring jstr) { + const jchar *pJChars = pEnv->GetStringChars(jstr, NULL); + if (pJChars == NULL) { + return wstring(L""); + } + + wstring wstr(pJChars); + + pEnv->ReleaseStringChars(jstr, pJChars); + + return wstr; +} + +jstring GetJStringFromString(JNIEnv *pEnv, + const jchar *unicodeChars, jsize len) { + return pEnv->NewString(unicodeChars, len); +} + +wstring GetLongPath(wstring path) { + wstring result(L""); + + size_t len = path.length(); + if (len > 1) { + if (path.at(len - 1) == '\\') { + path.erase(len - 1); + } + } + + TCHAR *pBuffer = new TCHAR[BUFFER_SIZE]; + if (pBuffer != NULL) { + DWORD dwResult = GetLongPathName(path.c_str(), pBuffer, BUFFER_SIZE); + if (dwResult > 0 && dwResult < BUFFER_SIZE) { + result = wstring(pBuffer); + } else { + delete [] pBuffer; + pBuffer = new TCHAR[dwResult]; + if (pBuffer != NULL) { + DWORD dwResult2 = + GetLongPathName(path.c_str(), pBuffer, dwResult); + if (dwResult2 == (dwResult - 1)) { + result = wstring(pBuffer); + } + } + } + + if (pBuffer != NULL) { + delete [] pBuffer; + } + } + + return result; +} diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/Utils.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/Utils.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/Utils.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef UTILS_H +#define UTILS_H + +#include +#include "jni.h" + +using namespace std; + +wstring GetStringFromJString(JNIEnv *pEnv, jstring jstr); +jstring GetJStringFromString(JNIEnv *pEnv, const jchar *unicodeChars, + jsize len); + +wstring GetLongPath(wstring path); + +#endif // UTILS_H diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/VersionInfoSwap.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/VersionInfoSwap.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/VersionInfoSwap.cpp @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "VersionInfoSwap.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace std; + +/* + * [Property file] contains key/value pairs + * The swap tool uses these pairs to create new version resource + * + * See MSDN docs for VS_VERSIONINFO structure that + * depicts organization of data in this version resource + * https://msdn.microsoft.com/en-us/library/ms647001(v=vs.85).aspx + * + * The swap tool makes changes in [Executable file] + * The tool assumes that the executable file has no version resource + * and it adds new resource in the executable file. + * If the executable file has an existing version resource, then + * the existing version resource will be replaced with new one. + */ + +VersionInfoSwap::VersionInfoSwap(wstring executableProperties, + wstring launcher) { + m_executableProperties = executableProperties; + m_launcher = launcher; +} + +bool VersionInfoSwap::PatchExecutable() { + bool b = LoadFromPropertyFile(); + if (!b) { + return false; + } + + ByteBuffer buf; + b = CreateNewResource(&buf); + if (!b) { + return false; + } + + b = this->UpdateResource(buf.getPtr(), static_cast (buf.getPos())); + if (!b) { + return false; + } + + return true; +} + +bool VersionInfoSwap::LoadFromPropertyFile() { + wifstream stream(m_executableProperties.c_str()); + + const locale empty_locale = locale::empty(); + const locale utf8_locale = + locale(empty_locale, new codecvt_utf8()); + stream.imbue(utf8_locale); + + if (stream.is_open() == true) { + int lineNumber = 1; + while (stream.eof() == false) { + wstring line; + getline(stream, line); + + // # at the first character will comment out the line. + if (line.empty() == false && line[0] != '#') { + wstring::size_type pos = line.find('='); + if (pos != wstring::npos) { + wstring name = line.substr(0, pos); + wstring value = line.substr(pos + 1); + m_props[name] = value; + } + } + lineNumber++; + } + return true; + } + + return false; +} + +/* + * Creates new version resource + * + * MSND docs for VS_VERSION_INFO structure + * https://msdn.microsoft.com/en-us/library/ms647001(v=vs.85).aspx + */ +bool VersionInfoSwap::CreateNewResource(ByteBuffer *buf) { + size_t versionInfoStart = buf->getPos(); + buf->AppendWORD(0); + buf->AppendWORD(sizeof VS_FIXEDFILEINFO); + buf->AppendWORD(0); + buf->AppendString(TEXT("VS_VERSION_INFO")); + buf->Align(4); + + VS_FIXEDFILEINFO fxi; + if (!FillFixedFileInfo(&fxi)) { + return false; + } + buf->AppendBytes((BYTE*) & fxi, sizeof (VS_FIXEDFILEINFO)); + buf->Align(4); + + // String File Info + size_t stringFileInfoStart = buf->getPos(); + buf->AppendWORD(0); + buf->AppendWORD(0); + buf->AppendWORD(1); + buf->AppendString(TEXT("StringFileInfo")); + buf->Align(4); + + // String Table + size_t stringTableStart = buf->getPos(); + buf->AppendWORD(0); + buf->AppendWORD(0); + buf->AppendWORD(1); + + // "040904B0" = LANG_ENGLISH/SUBLANG_ENGLISH_US, Unicode CP + buf->AppendString(TEXT("040904B0")); + buf->Align(4); + + // Strings + vector keys; + for (map::const_iterator it = + m_props.begin(); it != m_props.end(); ++it) { + keys.push_back(it->first); + } + + for (size_t index = 0; index < keys.size(); index++) { + wstring name = keys[index]; + wstring value = m_props[name]; + + size_t stringStart = buf->getPos(); + buf->AppendWORD(0); + buf->AppendWORD(static_cast (value.length())); + buf->AppendWORD(1); + buf->AppendString(name); + buf->Align(4); + buf->AppendString(value); + buf->ReplaceWORD(stringStart, + static_cast (buf->getPos() - stringStart)); + buf->Align(4); + } + + buf->ReplaceWORD(stringTableStart, + static_cast (buf->getPos() - stringTableStart)); + buf->ReplaceWORD(stringFileInfoStart, + static_cast (buf->getPos() - stringFileInfoStart)); + + // VarFileInfo + size_t varFileInfoStart = buf->getPos(); + buf->AppendWORD(1); + buf->AppendWORD(0); + buf->AppendWORD(1); + buf->AppendString(TEXT("VarFileInfo")); + buf->Align(4); + + buf->AppendWORD(0x24); + buf->AppendWORD(0x04); + buf->AppendWORD(0x00); + buf->AppendString(TEXT("Translation")); + buf->Align(4); + // "000004B0" = LANG_NEUTRAL/SUBLANG_ENGLISH_US, Unicode CP + buf->AppendWORD(0x0000); + buf->AppendWORD(0x04B0); + + buf->ReplaceWORD(varFileInfoStart, + static_cast (buf->getPos() - varFileInfoStart)); + buf->ReplaceWORD(versionInfoStart, + static_cast (buf->getPos() - versionInfoStart)); + + return true; +} + +bool VersionInfoSwap::FillFixedFileInfo(VS_FIXEDFILEINFO *fxi) { + wstring fileVersion; + wstring productVersion; + int ret; + + fileVersion = m_props[TEXT("FileVersion")]; + productVersion = m_props[TEXT("ProductVersion")]; + + unsigned fv_1 = 0, fv_2 = 0, fv_3 = 0, fv_4 = 0; + unsigned pv_1 = 0, pv_2 = 0, pv_3 = 0, pv_4 = 0; + + ret = _stscanf_s(fileVersion.c_str(), + TEXT("%d.%d.%d.%d"), &fv_1, &fv_2, &fv_3, &fv_4); + if (ret <= 0 || ret > 4) { + return false; + } + + ret = _stscanf_s(productVersion.c_str(), + TEXT("%d.%d.%d.%d"), &pv_1, &pv_2, &pv_3, &pv_4); + if (ret <= 0 || ret > 4) { + return false; + } + + fxi->dwSignature = 0xFEEF04BD; + fxi->dwStrucVersion = 0x00010000; + + fxi->dwFileVersionMS = MAKELONG(fv_2, fv_1); + fxi->dwFileVersionLS = MAKELONG(fv_4, fv_3); + fxi->dwProductVersionMS = MAKELONG(pv_2, pv_1); + fxi->dwProductVersionLS = MAKELONG(pv_4, pv_3); + + fxi->dwFileFlagsMask = 0; + fxi->dwFileFlags = 0; + fxi->dwFileOS = VOS_NT_WINDOWS32; + + wstring exeExt = + m_launcher.substr(m_launcher.find_last_of(TEXT("."))); + if (exeExt == TEXT(".exe")) { + fxi->dwFileType = VFT_APP; + } else if (exeExt == TEXT(".dll")) { + fxi->dwFileType = VFT_DLL; + } else { + fxi->dwFileType = VFT_UNKNOWN; + } + fxi->dwFileSubtype = 0; + + fxi->dwFileDateLS = 0; + fxi->dwFileDateMS = 0; + + return true; +} + +/* + * Adds new resource in the executable + */ +bool VersionInfoSwap::UpdateResource(LPVOID lpResLock, DWORD size) { + + HANDLE hUpdateRes; + BOOL r; + + hUpdateRes = ::BeginUpdateResource(m_launcher.c_str(), FALSE); + if (hUpdateRes == NULL) { + return false; + } + + r = ::UpdateResource(hUpdateRes, + RT_VERSION, + MAKEINTRESOURCE(VS_VERSION_INFO), + MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), + lpResLock, + size); + + if (!r) { + return false; + } + + if (!::EndUpdateResource(hUpdateRes, FALSE)) { + return false; + } + + return true; +} diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/VersionInfoSwap.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/VersionInfoSwap.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/VersionInfoSwap.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef VERSIONINFOSWAP_H +#define VERSIONINFOSWAP_H + +#include "ByteBuffer.h" +#include + +using namespace std; + +class VersionInfoSwap { +public: + VersionInfoSwap(wstring executableProperties, wstring launcher); + + bool PatchExecutable(); + +private: + wstring m_executableProperties; + wstring m_launcher; + + map m_props; + + bool LoadFromPropertyFile(); + bool CreateNewResource(ByteBuffer *buf); + bool UpdateResource(LPVOID lpResLock, DWORD size); + bool FillFixedFileInfo(VS_FIXEDFILEINFO *fxi); +}; + +#endif // VERSIONINFOSWAP_H + diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/WinErrorHandling.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/WinErrorHandling.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/WinErrorHandling.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "WinErrorHandling.h" +#include "Log.h" +#include "SysInfo.h" +#include "FileUtils.h" + + +namespace { + +std::string makeMessage(const std::string& msg, const char* label, + const void* c, DWORD errorCode) { + std::ostringstream err; + err << (label ? label : "Some error") << " [" << errorCode << "]"; + + HMODULE hmodule = NULL; + if (c) { + GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS + | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(c), &hmodule); + + if (!hmodule) { + LOG_WARNING(tstrings::any() << "GetModuleHandleEx() failed for " + << c << " address."); + } + } + if (hmodule || !c) { + err << "(" << SysError::getSysErrorMessage(errorCode, hmodule) << ")"; + } + + return joinErrorMessages(msg, err.str()); +} + + +std::wstring getSystemMessageDescription(DWORD messageId, HMODULE moduleHandle) { + LPWSTR pMsg = NULL; + std::wstring descr; + + // we always retrieve UNICODE description from system, + // convert it to utf8 if UNICODE is not defined + + while (true) { + DWORD res = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER + | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS + | (moduleHandle != NULL ? FORMAT_MESSAGE_FROM_HMODULE : 0), + moduleHandle, messageId, 0, (LPWSTR)&pMsg, 0, NULL); + if (res > 0) { + // replace all non-printed chars with space + for (DWORD i=0; i0; i--) { + if (pMsg[i] > L' ' && pMsg[i] != L'.') { + break; + } + pMsg[i] = 0; + } + + descr = pMsg; + + LocalFree(pMsg); + } else { + // if we fail to get description for specific moduleHandle, + // try to get "common" description. + if (moduleHandle != NULL) { + moduleHandle = NULL; + continue; + } + descr = L"No description available"; + } + break; + } + + return descr; +} + +} // namespace + + +SysError::SysError(const tstrings::any& msg, const void* caller, DWORD ec, + const char* label): + +std::runtime_error(makeMessage(msg.str(), label, caller, ec)) { +} + +std::wstring SysError::getSysErrorMessage(DWORD errCode, HMODULE moduleHandle) { + tstrings::any msg; + msg << "system error " << errCode + << " (" << getSystemMessageDescription(errCode, moduleHandle) << ")"; + return msg.tstr(); +} + +std::wstring SysError::getComErrorMessage(HRESULT hr) { + HRESULT hrOrig = hr; + // for FACILITY_WIN32 facility we need to reset hiword + if(HRESULT_FACILITY(hr) == FACILITY_WIN32) { + hr = HRESULT_CODE(hr); + } + return tstrings::format(_T("COM error 0x%08X (%s)"), hrOrig, + getSystemMessageDescription(hr, NULL)); +} diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/WinErrorHandling.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/WinErrorHandling.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/WinErrorHandling.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +#ifndef WinErrorHandling_h +#define WinErrorHandling_h + + +#include "ErrorHandling.h" + + +class SysError : public std::runtime_error { +public: + SysError(const tstrings::any& msg, const void* caller, + DWORD errorCode=GetLastError(), const char* label="System error"); + + // returns string "system error (error_description)" + // in UNICODE is not defined, the string returned is utf8-encoded + static std::wstring getSysErrorMessage(DWORD errCode = GetLastError(), + HMODULE moduleHandle = NULL); + + // returns string "COM error 0x
(error_description)" + // in UNICODE is not defined, the string returned is utf8-encoded + static std::wstring getComErrorMessage(HRESULT hr); +}; + +#endif // #ifndef WinErrorHandling_h diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/WinSysInfo.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/WinSysInfo.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/WinSysInfo.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include + +#include "WinSysInfo.h" +#include "FileUtils.h" +#include "WinErrorHandling.h" + +#pragma comment(lib, "Shell32") + +namespace SysInfo { + +tstring getTempDir() { + std::vector buffer(MAX_PATH); + DWORD res = GetTempPath(static_cast(buffer.size()), buffer.data()); + if (res > buffer.size()) { + buffer.resize(res); + GetTempPath(static_cast(buffer.size()), buffer.data()); + } + return FileUtils::removeTrailingSlash(buffer.data()); +} + +namespace { + +template +tstring getSystemDirImpl(Func func, const std::string& label) { + std::vector buffer(MAX_PATH); + for (int i=0; i<2; i++) { + DWORD res = func(buffer.data(), static_cast(buffer.size())); + if (!res) { + JP_THROW(SysError(label + " failed", func)); + } + if (res < buffer.size()) { + return FileUtils::removeTrailingSlash(buffer.data()); + } + buffer.resize(res + 1); + } + JP_THROW("Unexpected reply from" + label); +} + +} // namespace + +tstring getSystem32Dir() { + return getSystemDirImpl(GetSystemDirectory, "GetSystemDirectory"); +} + +tstring getWIPath() { + return FileUtils::mkpath() << getSystem32Dir() << _T("msiexec.exe"); +} + +namespace { + +tstring getModulePath(HMODULE h) +{ + std::vector buf(MAX_PATH); + DWORD len = 0; + while (true) { + len = GetModuleFileName(h, buf.data(), (DWORD)buf.size()); + if (len < buf.size()) { + break; + } + // buffer is too small, increase it + buf.resize(buf.size() * 2); + } + + if (len == 0) { + // error occured + JP_THROW(SysError("GetModuleFileName failed", GetModuleFileName)); + } + return tstring(buf.begin(), buf.begin() + len); +} + +} // namespace + +tstring getProcessModulePath() { + return getModulePath(NULL); +} + +HMODULE getCurrentModuleHandle() +{ + // get module handle for the address of this function + LPCWSTR address = reinterpret_cast(getCurrentModuleHandle); + HMODULE hmodule = NULL; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS + | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, address, &hmodule)) + { + JP_THROW(SysError(tstrings::any() << "GetModuleHandleExW failed", + GetModuleHandleExW)); + } + return hmodule; +} + +tstring getCurrentModulePath() +{ + return getModulePath(getCurrentModuleHandle()); +} + +tstring_array getCommandArgs(CommandArgProgramNameMode progNameMode) +{ + int argc = 0; + tstring_array result; + + LPWSTR *parsedArgs = CommandLineToArgvW(GetCommandLineW(), &argc); + if (parsedArgs == NULL) { + JP_THROW(SysError("CommandLineToArgvW failed", CommandLineToArgvW)); + } + // the 1st element contains program name + for (int i = progNameMode == ExcludeProgramName ? 1 : 0; i < argc; i++) { + result.push_back(parsedArgs[i]); + } + LocalFree(parsedArgs); + + return result; +} + +namespace { + +tstring getEnvVariableImpl(const tstring& name, bool* errorOccured=0) { + std::vector buf(10); + SetLastError(ERROR_SUCCESS); + const DWORD size = GetEnvironmentVariable(name.c_str(), buf.data(), + DWORD(buf.size())); + if (GetLastError() == ERROR_ENVVAR_NOT_FOUND) { + if (errorOccured) { + *errorOccured = true; + return tstring(); + } + JP_THROW(SysError(tstrings::any() << "GetEnvironmentVariable(" + << name << ") failed. Variable not set", GetEnvironmentVariable)); + } + + if (size > buf.size()) { + buf.resize(size); + GetEnvironmentVariable(name.c_str(), buf.data(), DWORD(buf.size())); + if (GetLastError() != ERROR_SUCCESS) { + if (errorOccured) { + *errorOccured = true; + return tstring(); + } + JP_THROW(SysError(tstrings::any() << "GetEnvironmentVariable(" + << name << ") failed", GetEnvironmentVariable)); + } + } + + if (errorOccured) { + *errorOccured = false; + } + return tstring(buf.data()); +} + +} // namespace + +tstring getEnvVariable(const tstring& name) { + return getEnvVariableImpl(name); +} + +tstring getEnvVariable(const std::nothrow_t&, const tstring& name, + const tstring& defValue) { + bool errorOccured = false; + const tstring reply = getEnvVariableImpl(name, &errorOccured); + if (errorOccured) { + return defValue; + } + return reply; +} + +bool isEnvVariableSet(const tstring& name) { + TCHAR unused[1]; + SetLastError(ERROR_SUCCESS); + GetEnvironmentVariable(name.c_str(), unused, _countof(unused)); + return GetLastError() != ERROR_ENVVAR_NOT_FOUND; +} + +} // end of namespace SysInfo diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/WinSysInfo.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/WinSysInfo.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/WinSysInfo.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +#ifndef WINSYSINFO_H +#define WINSYSINFO_H + +#include "SysInfo.h" + + +// +// Windows specific SysInfo. +// +namespace SysInfo { + // gets Windows System folder. A typical path is C:\Windows\System32. + tstring getSystem32Dir(); + + // returns full path to msiexec.exe executable + tstring getWIPath(); + + // Returns handle of the current module (exe or dll). + // The function assumes this code is statically linked to the module. + HMODULE getCurrentModuleHandle(); +} + + +#endif // WINSYSINFO_H diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/WindowsRegistry.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/WindowsRegistry.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/WindowsRegistry.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include +#include +#include + +#include "Utils.h" + +// Max value name size per MSDN plus NULL +#define VALUE_NAME_SIZE 16384 + +#ifdef __cplusplus +extern "C" { +#endif +#undef jdk_incubator_jpackage_internal_WindowsRegistry_HKEY_LOCAL_MACHINE +#define jdk_incubator_jpackage_internal_WindowsRegistry_HKEY_LOCAL_MACHINE 1L + + /* + * Class: jdk_incubator_jpackage_internal_WindowsRegistry + * Method: readDwordValue + * Signature: (ILjava/lang/String;Ljava/lang/String;I)I + */ + JNIEXPORT jint JNICALL + Java_jdk_incubator_jpackage_internal_WindowsRegistry_readDwordValue( + JNIEnv *pEnv, jclass c, jint key, jstring jSubKey, + jstring jValue, jint defaultValue) { + jint jResult = defaultValue; + + if (key != jdk_incubator_jpackage_internal_WindowsRegistry_HKEY_LOCAL_MACHINE) { + return jResult; + } + + wstring subKey = GetStringFromJString(pEnv, jSubKey); + wstring value = GetStringFromJString(pEnv, jValue); + + HKEY hSubKey = NULL; + LSTATUS status = RegOpenKeyEx(HKEY_LOCAL_MACHINE, subKey.c_str(), 0, + KEY_QUERY_VALUE, &hSubKey); + if (status == ERROR_SUCCESS) { + DWORD dwValue = 0; + DWORD cbData = sizeof (DWORD); + status = RegQueryValueEx(hSubKey, value.c_str(), NULL, NULL, + (LPBYTE) & dwValue, &cbData); + if (status == ERROR_SUCCESS) { + jResult = (jint) dwValue; + } + + RegCloseKey(hSubKey); + } + + return jResult; + } + + /* + * Class: jdk_incubator_jpackage_internal_WindowsRegistry + * Method: openRegistryKey + * Signature: (ILjava/lang/String;)J + */ + JNIEXPORT jlong JNICALL + Java_jdk_incubator_jpackage_internal_WindowsRegistry_openRegistryKey( + JNIEnv *pEnv, jclass c, jint key, jstring jSubKey) { + if (key != jdk_incubator_jpackage_internal_WindowsRegistry_HKEY_LOCAL_MACHINE) { + return 0; + } + + wstring subKey = GetStringFromJString(pEnv, jSubKey); + HKEY hSubKey = NULL; + LSTATUS status = RegOpenKeyEx(HKEY_LOCAL_MACHINE, subKey.c_str(), 0, + KEY_QUERY_VALUE, &hSubKey); + if (status == ERROR_SUCCESS) { + return (jlong)hSubKey; + } + + return 0; + } + + /* + * Class: jdk_incubator_jpackage_internal_WindowsRegistry + * Method: enumRegistryValue + * Signature: (JI)Ljava/lang/String; + */ + JNIEXPORT jstring JNICALL + Java_jdk_incubator_jpackage_internal_WindowsRegistry_enumRegistryValue( + JNIEnv *pEnv, jclass c, jlong lKey, jint jIndex) { + HKEY hKey = (HKEY)lKey; + TCHAR valueName[VALUE_NAME_SIZE] = {0}; // Max size per MSDN plus NULL + DWORD cchValueName = VALUE_NAME_SIZE; + LSTATUS status = RegEnumValue(hKey, (DWORD)jIndex, valueName, + &cchValueName, NULL, NULL, NULL, NULL); + if (status == ERROR_SUCCESS) { + size_t chLength = 0; + if (StringCchLength(valueName, VALUE_NAME_SIZE, &chLength) + == S_OK) { + return GetJStringFromString(pEnv, valueName, (jsize)chLength); + } + } + + return NULL; + } + + /* + * Class: jdk_incubator_jpackage_internal_WindowsRegistry + * Method: closeRegistryKey + * Signature: (J)V + */ + JNIEXPORT void JNICALL + Java_jdk_incubator_jpackage_internal_WindowsRegistry_closeRegistryKey( + JNIEnv *pEnc, jclass c, jlong lKey) { + HKEY hKey = (HKEY)lKey; + RegCloseKey(hKey); + } + + /* + * Class: jdk_incubator_jpackage_internal_WindowsRegistry + * Method: comparePaths + * Signature: (Ljava/lang/String;Ljava/lang/String;)Z + */ + JNIEXPORT jboolean JNICALL + Java_jdk_incubator_jpackage_internal_WindowsRegistry_comparePaths( + JNIEnv *pEnv, jclass c, jstring jPath1, jstring jPath2) { + wstring path1 = GetStringFromJString(pEnv, jPath1); + wstring path2 = GetStringFromJString(pEnv, jPath2); + + path1 = GetLongPath(path1); + path2 = GetLongPath(path2); + + if (path1.length() == 0 || path2.length() == 0) { + return JNI_FALSE; + } + + if (path1.length() != path2.length()) { + return JNI_FALSE; + } + + if (_tcsnicmp(path1.c_str(), path2.c_str(), path1.length()) == 0) { + return JNI_TRUE; + } + + return JNI_FALSE; + } + +#ifdef __cplusplus +} +#endif diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/jpackage.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/jpackage.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/jpackage.cpp @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2011, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include +#include +#include + +#include "ResourceEditor.h" +#include "WinErrorHandling.h" +#include "IconSwap.h" +#include "VersionInfoSwap.h" +#include "Utils.h" + +using namespace std; + +#ifdef __cplusplus +extern "C" { +#endif + + /* + * Class: jdk_incubator_jpackage_internal_WindowsAppImageBuilder + * Method: iconSwap + * Signature: (Ljava/lang/String;Ljava/lang/String;)I + */ + JNIEXPORT jint JNICALL + Java_jdk_incubator_jpackage_internal_WindowsAppImageBuilder_iconSwap( + JNIEnv *pEnv, jclass c, jstring jIconTarget, jstring jLauncher) { + wstring iconTarget = GetStringFromJString(pEnv, jIconTarget); + wstring launcher = GetStringFromJString(pEnv, jLauncher); + + if (ChangeIcon(iconTarget, launcher)) { + return 0; + } + + return 1; + } + + /* + * Class: jdk_incubator_jpackage_internal_WindowsAppImageBuilder + * Method: versionSwap + * Signature: (Ljava/lang/String;Ljava/lang/String;)I + */ + JNIEXPORT jint JNICALL + Java_jdk_incubator_jpackage_internal_WindowsAppImageBuilder_versionSwap( + JNIEnv *pEnv, jclass c, jstring jExecutableProperties, + jstring jLauncher) { + + wstring executableProperties = GetStringFromJString(pEnv, + jExecutableProperties); + wstring launcher = GetStringFromJString(pEnv, jLauncher); + + VersionInfoSwap vs(executableProperties, launcher); + if (vs.PatchExecutable()) { + return 0; + } + + return 1; + } + + /* + * Class: jdk_incubator_jpackage_internal_WinExeBundler + * Method: embedMSI + * Signature: (Ljava/lang/String;Ljava/lang/String;)I + */ + JNIEXPORT jint JNICALL Java_jdk_incubator_jpackage_internal_WinExeBundler_embedMSI( + JNIEnv *pEnv, jclass c, jstring jexePath, jstring jmsiPath) { + + const wstring exePath = GetStringFromJString(pEnv, jexePath); + const wstring msiPath = GetStringFromJString(pEnv, jmsiPath); + + JP_TRY; + + ResourceEditor() + .id(L"msi") + .type(RT_RCDATA) + .apply(ResourceEditor::FileLock(exePath), msiPath); + + return 0; + + JP_CATCH_ALL; + + return 1; + } + + BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, + LPVOID lpvReserved) { + return TRUE; + } + +#ifdef __cplusplus +} +#endif diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/tstrings.cpp b/src/jdk.incubator.jpackage/windows/native/libjpackage/tstrings.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/tstrings.cpp @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include +#include +#include + +#include "tstrings.h" +#include "ErrorHandling.h" + + +namespace tstrings { + +/* Create formatted string + */ +tstring unsafe_format(tstring::const_pointer format, ...) { + if (!format) { + throw std::invalid_argument("Destination buffer can't be NULL"); + } + + tstring fmtout; + int ret; + const int inc = 256; + + va_list args; + va_start(args, format); + do { + fmtout.resize(fmtout.size() + inc); +#ifdef _MSC_VER + ret = _vsntprintf_s(&*fmtout.begin(), fmtout.size(), _TRUNCATE, format, args); +#else + // With g++ this compiles only with '-std=gnu++0x' option + ret = vsnprintf(&*fmtout.begin(), fmtout.size(), format, args); +#endif + } while(-1 == ret); + va_end(args); + + //update string size by actual value + fmtout.resize(ret); + + return fmtout; +} + +/* + * Tests if two strings are equal according to CompareType. + * + * a - string to compare + * b - string to compare + * ct - CASE_SENSITIVE: case sensitive comparing type + * IGNORE_CASE: case insensitive comparing type + */ +bool equals(const tstring& a, const tstring& b, const CompareType ct) { + if (IGNORE_CASE==ct) { + return toLower(a) == toLower(b); + } + return a == b; +} + +bool startsWith(const tstring &str, const tstring &substr, const CompareType ct) +{ + if (str.size() < substr.size()) { + return false; + } + const tstring startOfStr = str.substr(0, substr.size()); + return tstrings::equals(startOfStr, substr, ct); +} + +bool endsWith(const tstring &str, const tstring &substr, const CompareType ct) +{ + if (str.size() < substr.size()) { + return false; + } + const tstring endOfStr = str.substr(str.size() - substr.size()); + return tstrings::equals(endOfStr, substr, ct); +} + +/* + * Split string into a vector with given delimiter string + * + * strVector - string vector to store split tstring + * str - string to split + * delimiter - delimiter to split the string around + * st - ST_ALL: return value includes an empty string + * ST_EXCEPT_EMPTY_STRING: return value does not include an empty string + * + * Note: It does not support multiple delimiters + */ +void split(tstring_array &strVector, const tstring &str, + const tstring &delimiter, const SplitType st) { + tstring::size_type start = 0, end = 0, length = str.length(); + + if (length == 0 || delimiter.length() == 0) { + return; + } + + end = str.find(delimiter, start); + while(end != tstring::npos) { + if(st == ST_ALL || end - start > 1 ) { + strVector.push_back(str.substr(start, end == tstring::npos ? + tstring::npos : end - start)); + } + start = end > (tstring::npos - delimiter.size()) ? + tstring::npos : end + delimiter.size(); + end = str.find(delimiter, start); + } + + if(st == ST_ALL || start < length) { + strVector.push_back(str.substr(start, length - start)); + } +} + +/* + * Convert uppercase letters to lowercase + */ +tstring toLower(const tstring& str) { + tstring lower(str); + tstring::iterator ok = std::transform(lower.begin(), lower.end(), + lower.begin(), tolower); + if (ok!=lower.end()) { + lower.resize(0); + } + return lower; +} + + +/* + * Replace all substring occurrences in a tstring. + * If 'str' or 'search' is empty the function returns 'str'. + * The given 'str' remains unchanged in any case. + * The function returns changed copy of 'str'. + */ +tstring replace(const tstring &str, const tstring &search, const tstring &replace) +{ + if (search.empty()) { + return str; + } + + tstring s(str); + + for (size_t pos = 0; ; pos += replace.length()) { + pos = s.find(search, pos); + if (pos == tstring::npos) { + break; + } + s.erase(pos, search.length()); + s.insert(pos, replace); + } + return s; +} + + +/* + * Remove trailing spaces + */ + +tstring trim(const tstring& str, const tstring& whitespace) { + const size_t strBegin = str.find_first_not_of(whitespace); + if (strBegin == std::string::npos) { + return tstring(); // no content + } + + const size_t strEnd = str.find_last_not_of(whitespace); + const size_t strRange = strEnd - strBegin + 1; + + return str.substr(strBegin, strRange); +} + +} // namespace tstrings + + +#ifdef TSTRINGS_WITH_WCHAR +namespace tstrings { + +namespace { +/* + * Converts UTF16-encoded string into multi-byte string of the given encoding. + */ +std::string toMultiByte(const std::wstring& utf16str, int encoding) { + std::string reply; + do { + int cm = WideCharToMultiByte(encoding, + 0, + utf16str.c_str(), + int(utf16str.size()), + NULL, + 0, + NULL, + NULL); + if (cm < 0) { + JP_THROW("Unexpected reply from WideCharToMultiByte()"); + } + if (0 == cm) { + break; + } + + reply.resize(cm); + int cm2 = WideCharToMultiByte(encoding, + 0, + utf16str.c_str(), + int(utf16str.size()), + &*reply.begin(), + cm, + NULL, + NULL); + if (cm != cm2) { + JP_THROW("Unexpected reply from WideCharToMultiByte()"); + } + } while(0); + + return reply; +} + +/* + * Converts multi-byte string of the given encoding into UTF16-encoded string. + */ +std::wstring fromMultiByte(const std::string& str, int encoding) { + std::wstring utf16; + do { + int cw = MultiByteToWideChar(encoding, + MB_ERR_INVALID_CHARS, + str.c_str(), + int(str.size()), + NULL, + 0); + if (cw < 0) { + JP_THROW("Unexpected reply from MultiByteToWideChar()"); + } + if (0 == cw) { + break; + } + + utf16.resize(cw); + int cw2 = MultiByteToWideChar(encoding, + MB_ERR_INVALID_CHARS, + str.c_str(), + int(str.size()), + &*utf16.begin(), + cw); + if (cw != cw2) { + JP_THROW("Unexpected reply from MultiByteToWideChar()"); + } + } while(0); + + return utf16; +} +} // namespace + +std::string toUtf8(const std::wstring& utf16str) { + return toMultiByte(utf16str, CP_UTF8); +} + +std::wstring toUtf16(const std::string& utf8str) { + return fromMultiByte(utf8str, CP_UTF8); +} + +} // namespace tstrings +#endif // ifdef TSTRINGS_WITH_WCHAR diff --git a/src/jdk.incubator.jpackage/windows/native/libjpackage/tstrings.h b/src/jdk.incubator.jpackage/windows/native/libjpackage/tstrings.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libjpackage/tstrings.h @@ -0,0 +1,426 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef TSTRINGS_H +#define TSTRINGS_H + +#ifdef _MSC_VER +# define TSTRINGS_WITH_WCHAR +#endif + +#ifdef TSTRINGS_WITH_WCHAR +#include +#include +// Want compiler issue C4995 warnings for encounters of deprecated functions. +#include +#endif + +// STL's string header depends on deprecated functions. +// We don't care about warnings from STL header, so disable them locally. +#ifdef _MSC_VER +# pragma warning(push) +# pragma warning(disable:4995) +#endif + +#include +#include +#include +#include + +#ifdef _MSC_VER +# pragma warning(pop) +#endif + + +#ifndef _T +# define _T(x) x +#endif + + +#ifdef TSTRINGS_WITH_WCHAR +typedef std::wstring tstring; +typedef std::wostringstream tostringstream; +typedef std::wistringstream tistringstream; +typedef std::wstringstream tstringstream; +typedef std::wistream tistream; +typedef std::wostream tostream; +typedef std::wiostream tiostream; +typedef std::wios tios; +#else +typedef std::string tstring; +typedef std::ostringstream tostringstream; +typedef std::istringstream tistringstream; +typedef std::stringstream tstringstream; +typedef std::istream tistream; +typedef std::ostream tostream; +typedef std::iostream tiostream; +typedef std::ios tios; + +typedef const char* LPCTSTR; +typedef char TCHAR; +#endif + +// frequently used "array of tstrings" type +typedef std::vector tstring_array; + +namespace tstrings { + tstring unsafe_format(tstring::const_pointer format, ...); + + enum CompareType {CASE_SENSITIVE, IGNORE_CASE}; + bool equals(const tstring& a, const tstring& b, + const CompareType ct=CASE_SENSITIVE); + bool startsWith(const tstring &str, const tstring &substr, + const CompareType ct=CASE_SENSITIVE); + bool endsWith(const tstring &str, const tstring &substr, + const CompareType ct=CASE_SENSITIVE); + + enum SplitType {ST_ALL, ST_EXCEPT_EMPTY_STRING}; + void split(tstring_array &strVector, const tstring &str, + const tstring &delimiter, const SplitType st = ST_ALL); + inline tstring_array split(const tstring &str, const tstring &delimiter, + const SplitType st = ST_ALL) { + tstring_array result; + split(result, str, delimiter, st); + return result; + } + tstring trim(const tstring& str, const tstring& whitespace = _T(" \t")); + + /** + * Writes sequence of values from [b, e) range into string buffer inserting + * 'delimiter' after each value except of the last one. + * Returns contents of string buffer. + */ + template + tstring join(It b, It e, const tstring& delimiter=tstring()) { + tostringstream buf; + if (b != e) { + for (;;) { + buf << *b; + if (++b == e) { + break; + } + buf << delimiter; + } + } + return buf.str(); + } + + tstring toLower(const tstring& str); + + tstring replace(const tstring &str, const tstring &search, + const tstring &replace); +} + + +namespace tstrings { + inline std::string toUtf8(const std::string& utf8str) { + return utf8str; + } + +#ifdef TSTRINGS_WITH_WCHAR + // conversion to Utf8 + std::string toUtf8(const std::wstring& utf16str); + + // conversion to Utf16 + std::wstring toUtf16(const std::string& utf8str); + + inline std::wstring fromUtf8(const std::string& utf8str) { + return toUtf16(utf8str); + } + +#else + inline std::string fromUtf8(const std::string& utf8str) { + return utf8str; + } +#endif +} // namespace tstrings + + +namespace tstrings { +namespace format_detail { + + template + struct str_arg_value { + const tstring value; + + str_arg_value(const std::string& v): value(fromUtf8(v)) { + } + +#ifdef TSTRINGS_WITH_WCHAR + str_arg_value(const std::wstring& v): value(v) { + } +#endif + + tstring::const_pointer operator () () const { + return value.c_str(); + } + }; + + template <> + struct str_arg_value { + const tstring::const_pointer value; + + str_arg_value(const tstring& v): value(v.c_str()) { + } + + str_arg_value(tstring::const_pointer v): value(v) { + } + + tstring::const_pointer operator () () const { + return value; + } + }; + + inline str_arg_value arg(const std::string& v) { + return v; + } + + inline str_arg_value arg(std::string::const_pointer v) { + return (v ? v : "(null)"); + } + +#ifdef TSTRINGS_WITH_WCHAR + inline str_arg_value arg(const std::wstring& v) { + return v; + } + + inline str_arg_value arg(std::wstring::const_pointer v) { + return (v ? v : L"(null)"); + } +#else + void arg(const std::wstring&); // Compilation error by design. + void arg(std::wstring::const_pointer); // Compilation error by design. +#endif + + template + struct arg_value { + arg_value(const T v): v(v) { + } + T operator () () const { + return v; + } + private: + const T v; + }; + + inline arg_value arg(int v) { + return v; + } + inline arg_value arg(unsigned v) { + return v; + } + inline arg_value arg(long v) { + return v; + } + inline arg_value arg(unsigned long v) { + return v; + } + inline arg_value arg(long long v) { + return v; + } + inline arg_value arg(unsigned long long v) { + return v; + } + inline arg_value arg(float v) { + return v; + } + inline arg_value arg(double v) { + return v; + } + inline arg_value arg(bool v) { + return v; + } + inline arg_value arg(const void* v) { + return v; + } + +} // namespace format_detail +} // namespace tstrings + + +namespace tstrings { + template + inline tstring format(const tstring& fmt, const T& v, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6, const T7& v7) { + return unsafe_format(fmt.c_str(), format_detail::arg(v)(), + format_detail::arg(v2)(), + format_detail::arg(v3)(), + format_detail::arg(v4)(), + format_detail::arg(v5)(), + format_detail::arg(v6)(), + format_detail::arg(v7)()); + } + + template + inline tstring format(const tstring& fmt, const T& v, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6) { + return unsafe_format(fmt.c_str(), format_detail::arg(v)(), + format_detail::arg(v2)(), + format_detail::arg(v3)(), + format_detail::arg(v4)(), + format_detail::arg(v5)(), + format_detail::arg(v6)()); + } + + template + inline tstring format(const tstring& fmt, const T& v, const T2& v2, const T3& v3, const T4& v4, const T5& v5) { + return unsafe_format(fmt.c_str(), format_detail::arg(v)(), + format_detail::arg(v2)(), + format_detail::arg(v3)(), + format_detail::arg(v4)(), + format_detail::arg(v5)()); + } + + template + inline tstring format(const tstring& fmt, const T& v, const T2& v2, const T3& v3, const T4& v4) { + return unsafe_format(fmt.c_str(), format_detail::arg(v)(), + format_detail::arg(v2)(), + format_detail::arg(v3)(), + format_detail::arg(v4)()); + } + + template + inline tstring format(const tstring& fmt, const T& v, const T2& v2, const T3& v3) { + return unsafe_format(fmt.c_str(), format_detail::arg(v)(), + format_detail::arg(v2)(), + format_detail::arg(v3)()); + } + + template + inline tstring format(const tstring& fmt, const T& v, const T2& v2) { + return unsafe_format(fmt.c_str(), format_detail::arg(v)(), + format_detail::arg(v2)()); + + } + + template + inline tstring format(const tstring& fmt, const T& v) { + return unsafe_format(fmt.c_str(), format_detail::arg(v)()); + } +} // namespace tstrings + + +namespace tstrings { + /** + * Buffer that accepts both std::wstring and std::string instances doing + * encoding conversions behind the scenes. All std::string-s assumed to be + * UTF8-encoded, all std::wstring-s assumed to be UTF16-encoded. + */ + class any { + public: + any() { + } + + any(std::string::const_pointer msg) { + data << fromUtf8(msg); + } + + any(const std::string& msg) { + data << fromUtf8(msg); + } + +#ifdef TSTRINGS_WITH_WCHAR + any(std::wstring::const_pointer msg) { + data << msg; + } + + any(const std::wstring& msg) { + data << msg; + } + + any& operator << (const std::wstring& v) { + data << v; + return *this; + } + + // need this specialization instead std::wstring::pointer, + // otherwise LPWSTR is handled as abstract pointer (void*) + any& operator << (LPWSTR v) { + data << (v ? v : L"NULL"); + return *this; + } + + // need this specialization instead std::wstring::const_pointer, + // otherwise LPCWSTR is handled as abstract pointer (const void*) + any& operator << (LPCWSTR v) { + data << (v ? v : L"NULL"); + return *this; + } + + std::wstring wstr() const { + return data.str(); + } +#endif + + template + any& operator << (T v) { + data << v; + return *this; + } + + any& operator << (tostream& (*pf)(tostream&)) { + data << pf; + return *this; + } + + any& operator << (tios& (*pf)(tios&)) { + data << pf; + return *this; + } + + any& operator << (std::ios_base& (*pf)(std::ios_base&)) { + data << pf; + return *this; + } + + std::string str() const { + return toUtf8(data.str()); + } + + tstring tstr() const { + return data.str(); + } + + private: + tostringstream data; + }; + + inline tstring to_tstring(const any& val) { + return val.tstr(); + } +} // namespace tstrings + + +inline std::ostream& operator << (std::ostream& os, const tstrings::any& buf) { + os << buf.str(); + return os; +} + +#ifdef TSTRINGS_WITH_WCHAR +inline std::wostream& operator << (std::wostream& os, const tstrings::any& buf) { + os << buf.wstr(); + return os; +} +#endif + +#endif //TSTRINGS_H diff --git a/src/jdk.incubator.jpackage/windows/native/libwixhelper/libwixhelper.cpp b/src/jdk.incubator.jpackage/windows/native/libwixhelper/libwixhelper.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/libwixhelper/libwixhelper.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include +#include + +extern "C" { + +#ifdef JP_EXPORT_FUNCTION +#error Unexpected JP_EXPORT_FUNCTION define +#endif +#define JP_EXPORT_FUNCTION comment(linker, "/EXPORT:" __FUNCTION__ "=" __FUNCDNAME__) + + BOOL WINAPI DllMain(HINSTANCE hInst, ULONG ulReason, + LPVOID lpvReserved) { + return TRUE; + } + + BOOL DirectoryExist(TCHAR *szValue) { + DWORD attr = GetFileAttributes(szValue); + if (attr == INVALID_FILE_ATTRIBUTES) { + return FALSE; + } + + if (attr & FILE_ATTRIBUTE_DIRECTORY) { + return TRUE; + } + + return FALSE; + } + + UINT __stdcall CheckInstallDir(MSIHANDLE hInstall) { + #pragma JP_EXPORT_FUNCTION + + TCHAR *szValue = NULL; + DWORD cchSize = 0; + + UINT result = MsiGetProperty(hInstall, TEXT("INSTALLDIR"), + TEXT(""), &cchSize); + if (result == ERROR_MORE_DATA) { + cchSize = cchSize + 1; // NULL termination + szValue = new TCHAR[cchSize]; + if (szValue) { + result = MsiGetProperty(hInstall, TEXT("INSTALLDIR"), + szValue, &cchSize); + } else { + return ERROR_INSTALL_FAILURE; + } + } + + if (result != ERROR_SUCCESS) { + delete [] szValue; + return ERROR_INSTALL_FAILURE; + } + + if (DirectoryExist(szValue)) { + if (PathIsDirectoryEmpty(szValue)) { + MsiSetProperty(hInstall, TEXT("INSTALLDIR_VALID"), TEXT("1")); + } else { + MsiSetProperty(hInstall, TEXT("INSTALLDIR_VALID"), TEXT("0")); + } + } else { + MsiSetProperty(hInstall, TEXT("INSTALLDIR_VALID"), TEXT("1")); + } + + delete [] szValue; + + return ERROR_SUCCESS; + } +} diff --git a/src/jdk.incubator.jpackage/windows/native/msiwrapper/Executor.cpp b/src/jdk.incubator.jpackage/windows/native/msiwrapper/Executor.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/msiwrapper/Executor.cpp @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include "Executor.h" +#include "Log.h" +#include "WinErrorHandling.h" + + +namespace { + +void escapeArg(std::wstring& str) { + if (str.empty()) { + return; + } + + if (str.front() == L'\"' && str.back() == L'\"' && str.size() > 1) { + return; + } + + if (str.find_first_of(L" \t") != std::wstring::npos) { + str = L'"' + str + L'"'; + } +} + +} // namespace + + +std::wstring Executor::args() const { + tstring_array tmpArgs; + // argv[0] is the module name. + tmpArgs.push_back(appPath); + tmpArgs.insert(tmpArgs.end(), argsArray.begin(), argsArray.end()); + + std::for_each(tmpArgs.begin(), tmpArgs.end(), escapeArg); + return tstrings::join(tmpArgs.begin(), tmpArgs.end(), _T(" ")); +} + + +int Executor::execAndWaitForExit() const { + UniqueHandle h = startProcess(); + + const DWORD res = ::WaitForSingleObject(h.get(), INFINITE); + if (WAIT_FAILED == res) { + JP_THROW(SysError("WaitForSingleObject() failed", WaitForSingleObject)); + } + + DWORD exitCode = 0; + if (!GetExitCodeProcess(h.get(), &exitCode)) { + // Error reading process's exit code. + JP_THROW(SysError("GetExitCodeProcess() failed", GetExitCodeProcess)); + } + + const DWORD processId = GetProcessId(h.get()); + if (!processId) { + JP_THROW(SysError("GetProcessId() failed.", GetProcessId)); + } + + LOG_TRACE(tstrings::any() << "Process with PID=" << processId + << " terminated. Exit code=" << exitCode); + + return static_cast(exitCode); +} + + +UniqueHandle Executor::startProcess() const { + const std::wstring argsStr = args(); + + std::vector argsBuffer(argsStr.begin(), argsStr.end()); + argsBuffer.push_back(0); // terminating '\0' + + STARTUPINFO startupInfo; + ZeroMemory(&startupInfo, sizeof(startupInfo)); + startupInfo.cb = sizeof(startupInfo); + + PROCESS_INFORMATION processInfo; + ZeroMemory(&processInfo, sizeof(processInfo)); + + DWORD creationFlags = 0; + + if (!theVisible) { + // For GUI applications. + startupInfo.dwFlags |= STARTF_USESHOWWINDOW; + startupInfo.wShowWindow = SW_HIDE; + + // For console applications. + creationFlags |= CREATE_NO_WINDOW; + } + + tstrings::any msg; + msg << "CreateProcess(" << appPath << ", " << argsStr << ")"; + + if (!CreateProcess(appPath.c_str(), argsBuffer.data(), NULL, NULL, FALSE, + creationFlags, NULL, NULL, &startupInfo, &processInfo)) { + msg << " failed"; + JP_THROW(SysError(msg, CreateProcess)); + } + + msg << " succeeded; PID=" << processInfo.dwProcessId; + LOG_TRACE(msg); + + // Close unneeded handles immediately. + UniqueHandle(processInfo.hThread); + + // Return process handle. + return UniqueHandle(processInfo.hProcess); +} diff --git a/src/jdk.incubator.jpackage/windows/native/msiwrapper/Executor.h b/src/jdk.incubator.jpackage/windows/native/msiwrapper/Executor.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/msiwrapper/Executor.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef EXECUTOR_H +#define EXECUTOR_H + +#include "tstrings.h" +#include "UniqueHandle.h" + + +class Executor { +public: + explicit Executor(const std::wstring& appPath=std::wstring()) { + app(appPath).visible(false); + } + + /** + * Returns command line configured with arg() calls so far. + */ + std::wstring args() const; + + /** + * Set path to application to execute. + */ + Executor& app(const std::wstring& v) { + appPath = v; + return *this; + } + + /** + * Adds another command line argument. + */ + Executor& arg(const std::wstring& v) { + argsArray.push_back(v); + return *this; + } + + /** + * Controls if application window should be visible. + */ + Executor& visible(bool v) { + theVisible = v; + return *this; + } + + /** + * Starts application process and blocks waiting when the started + * process terminates. + * Returns process exit code. + * Throws exception if process start failed. + */ + int execAndWaitForExit() const; + +private: + UniqueHandle startProcess() const; + + bool theVisible; + tstring_array argsArray; + std::wstring appPath; +}; + +#endif // #ifndef EXECUTOR_H diff --git a/src/jdk.incubator.jpackage/windows/native/msiwrapper/MsiWrapper.cpp b/src/jdk.incubator.jpackage/windows/native/msiwrapper/MsiWrapper.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/msiwrapper/MsiWrapper.cpp @@ -0,0 +1,42 @@ +#include +#include + +#include "SysInfo.h" +#include "FileUtils.h" +#include "Executor.h" +#include "Resources.h" +#include "WinErrorHandling.h" + + +int __stdcall WinMain(HINSTANCE, HINSTANCE, LPSTR lpCmdLine, int nShowCmd) +{ + JP_TRY; + + // Create temporary directory where to extract msi file. + const auto tempMsiDir = FileUtils::createTempDirectory(); + + // Schedule temporary directory for deletion. + FileUtils::Deleter cleaner; + cleaner.appendRecursiveDirectory(tempMsiDir); + + const auto msiPath = FileUtils::mkpath() << tempMsiDir << L"main.msi"; + + // Extract msi file. + Resource(L"msi", RT_RCDATA).saveToFile(msiPath); + + // Setup executor to run msiexec + Executor msiExecutor(SysInfo::getWIPath()); + msiExecutor.arg(L"/i").arg(msiPath); + const auto args = SysInfo::getCommandArgs(); + std::for_each(args.begin(), args.end(), + [&msiExecutor] (const tstring& arg) { + msiExecutor.arg(arg); + }); + + // Install msi file. + return msiExecutor.execAndWaitForExit(); + + JP_CATCH_ALL; + + return -1; +} diff --git a/src/jdk.incubator.jpackage/windows/native/msiwrapper/Resources.cpp b/src/jdk.incubator.jpackage/windows/native/msiwrapper/Resources.cpp new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/msiwrapper/Resources.cpp @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "Resources.h" +#include "FileUtils.h" +#include "WinErrorHandling.h" + +#include + + +Resource::Resource(LPCTSTR name, LPCTSTR type, HINSTANCE module) { + init(name, type, module); +} + +Resource::Resource(UINT id, LPCTSTR type, HINSTANCE module) { + init(MAKEINTRESOURCE(id), type, module); +} + +void Resource::init(LPCTSTR name, LPCTSTR type, HINSTANCE module) { + if (IS_INTRESOURCE(name)) { + std::wostringstream printer; + printer << L"#" << reinterpret_cast(name); + nameStr = printer.str(); + namePtr = name; + } else { + nameStr = name; + namePtr = nameStr.c_str(); + } + if (IS_INTRESOURCE(type)) { + std::wostringstream printer; + printer << L"#" << reinterpret_cast(name); + typeStr = printer.str(); + typePtr = type; + } else { + typeStr = type; + typePtr = typeStr.c_str(); + } + instance = module; +} + +std::string Resource::getErrMsg(const std::string &descr) const { + return (tstrings::any() << descr << " (name='" << nameStr << + "', type='" << typeStr << "')").str(); +} + +HRSRC Resource::findResource() const { + LPCTSTR id = namePtr; + // string resources are stored in blocks (stringtables) + // id of the resource is (stringId / 16 + 1) + if (typePtr == RT_STRING) { + id = MAKEINTRESOURCE(UINT(size_t(id) / 16 + 1)); + } + return FindResource(instance, id, typePtr); +} + +LPVOID Resource::getPtr(DWORD &size) const +{ + // LoadString returns the same result if value is zero-length or + // if if the value does not exists, + // so wee need to ensure the stringtable exists + HRSRC resInfo = findResource(); + if (resInfo == NULL) { + JP_THROW(SysError(getErrMsg("cannot find resource"), FindResource)); + } + + HGLOBAL res = LoadResource(instance, resInfo); + if (res == NULL) { + JP_THROW(SysError(getErrMsg("cannot load resource"), LoadResource)); + } + + LPVOID ptr = LockResource(res); + if (res == NULL) { + JP_THROW(SysError(getErrMsg("cannot lock resource"), LockResource)); + } + + if (typePtr == RT_STRING) { + // string resources are stored in stringtables and + // need special handling + // The simplest way (while we don't need handle resource locale) + // is LoadString + // But this adds dependency on user32.dll, + // so implement custom string extraction + + // number in the block (namePtr is an integer) + size_t num = size_t(namePtr) & 0xf; + LPWSTR strPtr = (LPWSTR)ptr; + for (size_t i = 0; i < num; i++) { + // 1st symbol contains string length + strPtr += DWORD(*strPtr) + 1; + } + // *strPtr contains string length, string value starts at strPtr+1 + size = DWORD(*strPtr) * sizeof(wchar_t); + ptr = strPtr+1; + } else { + size = SizeofResource(instance, resInfo); + } + + return ptr; +} + +bool Resource::available() const { + return NULL != findResource(); +} + +unsigned Resource::size() const { + DWORD size = 0; + getPtr(size); + return size; +} + +LPCVOID Resource::rawData() const { + DWORD size = 0; + return getPtr(size); +} + +void Resource::saveToFile(const std::wstring &filePath) const { + DWORD size = 0; + const char *resPtr = (const char *)getPtr(size); + + FileUtils::FileWriter(filePath).write(resPtr, size).finalize(); +} + +Resource::ByteArray Resource::binary() const { + DWORD size = 0; + LPBYTE resPtr = (LPBYTE)getPtr(size); + return ByteArray(resPtr, resPtr+size); +} diff --git a/src/jdk.incubator.jpackage/windows/native/msiwrapper/Resources.h b/src/jdk.incubator.jpackage/windows/native/msiwrapper/Resources.h new file mode 100644 --- /dev/null +++ b/src/jdk.incubator.jpackage/windows/native/msiwrapper/Resources.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef RESOURCES_H +#define RESOURCES_H + +#include "WinSysInfo.h" + + +/** + * Classes for resource loading. + * Common use cases: + * - check if resource is available and save it to file: + * Resource res(_T("MyResource"), _T("CustomResourceType")); + * if (res.available()) { + * res.saveToFile(_T("c:\\temp\\my_resource.bin")); + * } + */ + +class Resource { +public: + // name and type can be specified by string id, + // by integer id (RT_* constants or MAKEINTRESOURCE) + Resource(LPCWSTR name, LPCWSTR type, + HINSTANCE module = SysInfo::getCurrentModuleHandle()); + Resource(UINT id, LPCWSTR type, + HINSTANCE module = SysInfo::getCurrentModuleHandle()); + + bool available() const; + + // all this methods throw exception if the resource is not available + unsigned size() const; + // gets raw pointer to the resource data + LPCVOID rawData() const; + + // save the resource to a file + void saveToFile(const std::wstring &filePath) const; + + typedef std::vector ByteArray; + // returns the resource as byte array + ByteArray binary() const; + +private: + std::wstring nameStr; + LPCWSTR namePtr; // can be integer value or point to nameStr.c_str() + std::wstring typeStr; + LPCWSTR typePtr; // can be integer value or point to nameStr.c_str() + HINSTANCE instance; + + void init(LPCWSTR name, LPCWSTR type, HINSTANCE module); + + // generates error message + std::string getErrMsg(const std::string &descr) const; + HRSRC findResource() const; + LPVOID getPtr(DWORD &size) const; + +private: + // disable copying + Resource(const Resource&); + Resource& operator = (const Resource&); +}; + +#endif // RESOURCES_H diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/packager/AppRuntimeImageBuilder.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/packager/AppRuntimeImageBuilder.java deleted file mode 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/packager/AppRuntimeImageBuilder.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package jdk.tools.jlink.internal.packager; - - -import jdk.tools.jlink.builder.DefaultImageBuilder; -import jdk.tools.jlink.internal.Jlink; -import jdk.tools.jlink.internal.JlinkTask; -import jdk.tools.jlink.plugin.Plugin; - -import java.io.File; -import java.io.IOException; -import java.lang.module.ModuleFinder; -import java.nio.ByteOrder; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * AppRuntimeImageBuilder is a private API used only by the Java Packager to generate - * a Java runtime image using jlink. AppRuntimeImageBuilder encapsulates the - * arguments that jlink requires to generate this image. To create the image call the - * build() method. - */ -public final class AppRuntimeImageBuilder { - private Path outputDir = null; - private Map launchers = Collections.emptyMap(); - private List modulePath = null; - private Set addModules = null; - private Set limitModules = null; - private String excludeFileList = null; - private Map userArguments = null; - private Boolean stripNativeCommands = null; - - public AppRuntimeImageBuilder() {} - - public void setOutputDir(Path value) { - outputDir = value; - } - - public void setLaunchers(Map value) { - launchers = value; - } - - public void setModulePath(List value) { - modulePath = value; - } - - public void setAddModules(Set value) { - addModules = value; - } - - public void setLimitModules(Set value) { - limitModules = value; - } - - public void setExcludeFileList(String value) { - excludeFileList = value; - } - - public void setStripNativeCommands(boolean value) { - stripNativeCommands = value; - } - - public void setUserArguments(Map value) { - userArguments = value; - } - - public void build() throws IOException { - // jlink main arguments - Jlink.JlinkConfiguration jlinkConfig = - new Jlink.JlinkConfiguration(new File("").toPath(), // Unused - addModules, - ByteOrder.nativeOrder(), - moduleFinder(modulePath, - limitModules, addModules)); - - // plugin configuration - List plugins = new ArrayList(); - - if (stripNativeCommands) { - plugins.add(Jlink.newPlugin( - "strip-native-commands", - Collections.singletonMap("strip-native-commands", "on"), - null)); - } - - if (excludeFileList != null && !excludeFileList.isEmpty()) { - plugins.add(Jlink.newPlugin( - "exclude-files", - Collections.singletonMap("exclude-files", excludeFileList), - null)); - } - - // add user supplied jlink arguments - for (Map.Entry entry : userArguments.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - plugins.add(Jlink.newPlugin(key, - Collections.singletonMap(key, value), - null)); - } - - // build the image - Jlink.PluginsConfiguration pluginConfig = new Jlink.PluginsConfiguration( - plugins, new DefaultImageBuilder(outputDir, launchers), null); - Jlink jlink = new Jlink(); - jlink.build(jlinkConfig, pluginConfig); - } - - /* - * Returns a ModuleFinder that limits observability to the given root - * modules, their transitive dependences, plus a set of other modules. - */ - public static ModuleFinder moduleFinder(List modulepaths, - Set roots, - Set otherModules) { - return JlinkTask.newModuleFinder(modulepaths, roots, otherModules); - } -} diff --git a/test/jdk/tools/jpackage/TEST.properties b/test/jdk/tools/jpackage/TEST.properties new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/TEST.properties @@ -0,0 +1,3 @@ +keys=jpackagePlatformPackage +requires.properties=jpackage.test.SQETest +maxOutputSize=2000000 diff --git a/test/jdk/tools/jpackage/apps/com.hello/com/hello/Hello.java b/test/jdk/tools/jpackage/apps/com.hello/com/hello/Hello.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/apps/com.hello/com/hello/Hello.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.hello; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.PrintWriter; + +public class Hello { + + private static final String MSG = "jpackage test application"; + private static final int EXPECTED_NUM_OF_PARAMS = 3; // Starts at 1 + + public static void main(String[] args) { + String outputFile = "appOutput.txt"; + File file = new File(outputFile); + + try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)))) { + System.out.println(MSG); + out.println(MSG); + + System.out.println("args.length: " + args.length); + out.println("args.length: " + args.length); + + for (String arg : args) { + System.out.println(arg); + out.println(arg); + } + + for (int index = 1; index <= EXPECTED_NUM_OF_PARAMS; index++) { + String value = System.getProperty("param" + index); + if (value != null) { + System.out.println("-Dparam" + index + "=" + value); + out.println("-Dparam" + index + "=" + value); + } + } + } catch (Exception ex) { + System.err.println(ex.toString()); + } + } + +} diff --git a/test/jdk/tools/jpackage/apps/com.hello/module-info.java b/test/jdk/tools/jpackage/apps/com.hello/module-info.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/apps/com.hello/module-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +module com.hello { + exports com.hello; +} diff --git a/test/jdk/tools/jpackage/apps/com.other/com/other/Other.java b/test/jdk/tools/jpackage/apps/com.other/com/other/Other.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/apps/com.other/com/other/Other.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.other; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.PrintWriter; + +public class Other { + + private static final String MSG = "other jpackage test application"; + private static final int EXPECTED_NUM_OF_PARAMS = 3; // Starts at 1 + + public static void main(String[] args) { + String outputFile = "appOutput.txt"; + File file = new File(outputFile); + + try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)))) { + System.out.println(MSG); + out.println(MSG); + + System.out.println("args.length: " + args.length); + out.println("args.length: " + args.length); + + for (String arg : args) { + System.out.println(arg); + out.println(arg); + } + + for (int index = 1; index <= EXPECTED_NUM_OF_PARAMS; index++) { + String value = System.getProperty("param" + index); + if (value != null) { + System.out.println("-Dparam" + index + "=" + value); + out.println("-Dparam" + index + "=" + value); + } + } + } catch (Exception ex) { + System.err.println(ex.toString()); + } + } + +} diff --git a/test/jdk/tools/jpackage/apps/com.other/module-info.java b/test/jdk/tools/jpackage/apps/com.other/module-info.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/apps/com.other/module-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +module com.other { + exports com.other; +} diff --git a/test/jdk/tools/jpackage/apps/dukeplug.png b/test/jdk/tools/jpackage/apps/dukeplug.png new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6b7a9c1cc2d53b962f7c1fefcdc08324d187dcc4 GIT binary patch literal 1396 zc$@)r1&jKLP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf02y>eSaefwW^{L9 za%BKPWN%_+AW3auXJt}lVPtu6$z?nM00iVoL_t(oN3EAvNM%PDfPY01M2*4TLB*g( zB`9Ll2Nhc^Arc$?ZEXMQYweQ;68us`1i?$|O zBZY;9iO%cm>o_$vr4GdF#}aULb;arFY0SvTK!1OKq0`n+XC&SsA|e9a-QCpzcv)Fl zArIB)JjBz}6WiO{(Z|OJTUuH$CnrbrYiMX7kGJSdfOpV*FfVg+b7Vyv9v&7tCnqP; zs3<5Xz__?LG&D5C+}vE@6BHCgRw=s2lHum&rha&8YAR_FTU%SzdS0@#v&n)uF)<;r z@lse=NLDG2Ca|@&r4GVYw9t)>jY)#vmyL}LVdp%wd3$@4MT#C@V9m|VU~g{^78Vv@ zXJ-etwzlBn;sUFytB{zO$oi{b{=nuRe0_aMBZZ9?Y;JCfHXDApySo$Zd#bCep}4r1 zBqBXM9i5$>F)AubEFK332eh)Xl4NgcYAR~BFG%!Cg(ZNEjg5*nUXG8CRezm;?d@$e zHZ~?v&hqlIs6#?R6azjie6jJGjUr}cWnp}Lyv%22W=6#1h&51CQ-gkfenO!QHbt^P zy2{DP3H4O5xVVV5wY8X*mL}{)MMXldUW9C}#p|O)Wo4!80gsi3324IBmhjFMFn&=3v`42XYWZEY}_prLRL&wUteDqkQW~Z8#bB0;?>e? zyzFhpziVC~zR18&Ka`I|ettehQ_<1UA@q@vk)%EfsOtCPp5FDi0}5 zO-&-(&dyHKs7TAm#Fg*9M(lfqZ(hAb{GkBPz6?e`cQ^7t?C$QCjD(Sq5iTq&kX4S> z)nZ~|LM*!VbtQJz$52dwZx;MreJ);JUewZ#;v!^o2JG}A>Hp*5;Q4!T9(%Nm`(hP!k>=u0Hpl%ii7|CMPG8q>O9?_V@R}%*>3W=i%t+ zDB|ftl0aWyALQlbk@S8zJ3FJ67HA{{F)=a7K0-)(KkyS)^A`OO$q2C93YeRllO%`j za!^)Q1{D<*;=1+}*q?&Y(NU7*ND^R&5VhXIE~UUeQp7!w&nLela=EIi3fKn-uv3#f zl?e?Eh1uCzk`$nkK<0$(z#vJrf&&5q#FdIYn6z^E5c4}Fzc(mxb8{muJKB&Wz)nFN z85zOo=xAh_izJ=`UpG8i{>rfrSJJBBm(}}vd3gy52?=0n$&TK4 zgzuo`<>lh*IXE~N4i67ua&i*b_azt@7?77H0RI8_kOOc5^zo|z0000 printArgs(String[] args) { + List lines = new ArrayList<>(); + lines.add(MSG); + + lines.add("args.length: " + args.length); + + lines.addAll(List.of(args)); + + for (int index = 1; index <= EXPECTED_NUM_OF_PARAMS; index++) { + String value = System.getProperty("param" + index); + if (value != null) { + lines.add("-Dparam" + index + "=" + value); + } + } + + return lines; + } + + private static Path getOutputFile(String[] args) { + Path outputFilePath = Path.of("appOutput.txt"); + + // If first arg is a file (most likely from fa), then put output in the same folder as + // the file from fa. + if (args.length >= 1) { + Path faPath = Path.of(args[0]); + if (Files.exists(faPath)) { + return faPath.toAbsolutePath().getParent().resolve(outputFilePath); + } + } + + try { + // Try writing in the default output file. + Files.write(outputFilePath, Collections.emptyList()); + return outputFilePath; + } catch (IOException ex) { + // Log reason of a failure. + StringWriter errors = new StringWriter(); + ex.printStackTrace(new PrintWriter(errors)); + Stream.of(errors.toString().split("\\R")).forEachOrdered(Hello::trace); + } + + return Path.of(System.getProperty("user.home")).resolve(outputFilePath); + } + + @Override + public void openFiles(OpenFilesEvent e) { + synchronized(lock) { + trace("openFiles"); + files = e.getFiles().stream() + .map(File::toString) + .collect(Collectors.toList()); + + lock.notifyAll(); + } + } + + private static List getFaFiles() throws InterruptedException { + if (openFilesHandler == null) { + return null; + } + + synchronized(openFilesHandler.lock) { + trace("getFaFiles: wait"); + openFilesHandler.lock.wait(1000); + if (openFilesHandler.files == null) { + trace(String.format("getFaFiles: no files")); + return null; + } + // Return copy of `files` to keep access to `files` field synchronized. + trace(String.format("getFaFiles: file count %d", + openFilesHandler.files.size())); + return new ArrayList<>(openFilesHandler.files); + } + } + + private List files; + private final Object lock = new Object(); + private final static Hello openFilesHandler = createInstance(); + + private static Hello createInstance() { + if (GraphicsEnvironment.isHeadless()) { + return null; + } + + trace("Environment supports a display"); + + try { + // Disable JAB. + // Needed to suppress error: + // Exception in thread "main" java.awt.AWTError: Assistive Technology not found: com.sun.java.accessibility.AccessBridge + System.setProperty("javax.accessibility.assistive_technologies", ""); + } catch (SecurityException ex) { + ex.printStackTrace(); + } + + try { + var desktop = Desktop.getDesktop(); + if (desktop.isSupported(Desktop.Action.APP_OPEN_FILE)) { + trace("Set file handler"); + Hello instance = new Hello(); + desktop.setOpenFileHandler(instance); + return instance; + } + } catch (AWTError ex) { + trace("Set file handler failed"); + ex.printStackTrace(); + } + + return null; + } + + private static final String MSG = "jpackage test application"; + private static final int EXPECTED_NUM_OF_PARAMS = 3; // Starts at 1 + + private static void trace(String msg) { + System.out.println("hello: " + msg); + } +} diff --git a/test/jdk/tools/jpackage/apps/installer/Hello.java b/test/jdk/tools/jpackage/apps/installer/Hello.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/apps/installer/Hello.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.awt.Desktop; +import java.awt.desktop.OpenFilesEvent; +import java.awt.desktop.OpenFilesHandler; +import java.util.List; + +public class Hello implements OpenFilesHandler { + + private static final String MSG = "jpackage test application"; + private static final int EXPECTED_NUM_OF_PARAMS = 3; // Starts at 1 + private static List files; + + public static void main(String[] args) { + if(Desktop.getDesktop().isSupported(Desktop.Action.APP_OPEN_FILE)) { + Desktop.getDesktop().setOpenFileHandler(new Hello()); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + printToStdout(args); + if (args.length == 1 || (files != null && files.size() == 1)) { // Called via file association + printToFile(args); + } + } + + private static void printToStdout(String[] args) { + System.out.println(MSG); + + System.out.println("args.length: " + (files == null ? args.length : args.length + files.size())); + + for (String arg : args) { + System.out.println(arg); + } + + if (files != null) { + for (File file : files) { + System.out.println(file.getAbsolutePath()); + } + } + + for (int index = 1; index <= EXPECTED_NUM_OF_PARAMS; index++) { + String value = System.getProperty("param" + index); + if (value != null) { + System.out.println("-Dparam" + index + "=" + value); + } + } + } + + private static void printToFile(String[] args) { + File inputFile = files == null ? new File(args[0]) : files.get(0); + String outputFile = inputFile.getParent() + File.separator + "appOutput.txt"; + File file = new File(outputFile); + + try (PrintWriter out + = new PrintWriter(new BufferedWriter(new FileWriter(file)))) { + out.println(MSG); + + out.println("args.length: " + (files == null ? args.length : args.length + files.size())); + + for (String arg : args) { + out.println(arg); + } + + if (files != null) { + for (File f : files) { + out.println(f.getAbsolutePath()); + } + } + + for (int index = 1; index <= EXPECTED_NUM_OF_PARAMS; index++) { + String value = System.getProperty("param" + index); + if (value != null) { + out.println("-Dparam" + index + "=" + value); + } + } + } catch (Exception ex) { + System.err.println(ex.getMessage()); + } + } + + @Override + public void openFiles(OpenFilesEvent e) { + files = e.getFiles(); + } +} diff --git a/test/jdk/tools/jpackage/helpers/JPackageHelper.java b/test/jdk/tools/jpackage/helpers/JPackageHelper.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/JPackageHelper.java @@ -0,0 +1,683 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.BufferedWriter; +import java.nio.file.FileVisitResult; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import java.util.spi.ToolProvider; + +public class JPackageHelper { + + private static final boolean VERBOSE = false; + private static final String OS = System.getProperty("os.name").toLowerCase(); + private static final String JAVA_HOME = System.getProperty("java.home"); + public static final String TEST_SRC_ROOT; + public static final String TEST_SRC; + private static final Path BIN_DIR = Path.of(JAVA_HOME, "bin"); + private static final Path JPACKAGE; + private static final Path JAVAC; + private static final Path JAR; + private static final Path JLINK; + + public static class ModuleArgs { + private final String version; + private final String mainClass; + + ModuleArgs(String version, String mainClass) { + this.version = version; + this.mainClass = mainClass; + } + + public String getVersion() { + return version; + } + + public String getMainClass() { + return mainClass; + } + } + + static { + if (OS.startsWith("win")) { + JPACKAGE = BIN_DIR.resolve("jpackage.exe"); + JAVAC = BIN_DIR.resolve("javac.exe"); + JAR = BIN_DIR.resolve("jar.exe"); + JLINK = BIN_DIR.resolve("jlink.exe"); + } else { + JPACKAGE = BIN_DIR.resolve("jpackage"); + JAVAC = BIN_DIR.resolve("javac"); + JAR = BIN_DIR.resolve("jar"); + JLINK = BIN_DIR.resolve("jlink"); + } + + // Figure out test src based on where we called + TEST_SRC = System.getProperty("test.src"); + Path root = Path.of(TEST_SRC); + Path apps = Path.of(TEST_SRC, "apps"); + if (apps.toFile().exists()) { + // fine - test is at root + } else { + apps = Path.of(TEST_SRC, "..", "apps"); + if (apps.toFile().exists()) { + root = apps.getParent().normalize(); // test is 1 level down + } else { + apps = Path.of(TEST_SRC, "..", "..", "apps"); + if (apps.toFile().exists()) { + root = apps.getParent().normalize(); // 2 levels down + } else { + apps = Path.of(TEST_SRC, "..", "..", "..", "apps"); + if (apps.toFile().exists()) { + root = apps.getParent().normalize(); // 3 levels down + } else { + // if we ever have tests more than three levels + // down we need to add code here + throw new RuntimeException("we should never get here"); + } + } + } + } + TEST_SRC_ROOT = root.toString(); + } + + static final ToolProvider JPACKAGE_TOOL = + ToolProvider.findFirst("jpackage").orElseThrow( + () -> new RuntimeException("jpackage tool not found")); + + public static int execute(File out, String... command) throws Exception { + if (VERBOSE) { + System.out.print("Execute command: "); + for (String c : command) { + System.out.print(c); + System.out.print(" "); + } + System.out.println(); + } + + ProcessBuilder builder = new ProcessBuilder(command); + if (out != null) { + builder.redirectErrorStream(true); + builder.redirectOutput(out); + } + + Process process = builder.start(); + return process.waitFor(); + } + + public static Process executeNoWait(File out, String... command) throws Exception { + if (VERBOSE) { + System.out.print("Execute command: "); + for (String c : command) { + System.out.print(c); + System.out.print(" "); + } + System.out.println(); + } + + ProcessBuilder builder = new ProcessBuilder(command); + if (out != null) { + builder.redirectErrorStream(true); + builder.redirectOutput(out); + } + + return builder.start(); + } + + private static String[] getCommand(String... args) { + String[] command; + if (args == null) { + command = new String[1]; + } else { + command = new String[args.length + 1]; + } + + int index = 0; + command[index] = JPACKAGE.toString(); + + if (args != null) { + for (String arg : args) { + index++; + command[index] = arg; + } + } + + return command; + } + + public static void deleteRecursive(File path) throws IOException { + if (!path.exists()) { + return; + } + + Path directory = path.toPath(); + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attr) throws IOException { + file.toFile().setWritable(true); + if (OS.startsWith("win")) { + try { + Files.setAttribute(file, "dos:readonly", false); + } catch (Exception ioe) { + // just report and try to contune + System.err.println("IOException: " + ioe); + ioe.printStackTrace(System.err); + } + } + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, + BasicFileAttributes attr) throws IOException { + if (OS.startsWith("win")) { + Files.setAttribute(dir, "dos:readonly", false); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) + throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + public static void deleteOutputFolder(String output) throws IOException { + File outputFolder = new File(output); + System.out.println("deleteOutputFolder: " + outputFolder.getAbsolutePath()); + try { + deleteRecursive(outputFolder); + } catch (IOException ioe) { + System.err.println("IOException: " + ioe); + ioe.printStackTrace(System.err); + deleteRecursive(outputFolder); + } + } + + public static String executeCLI(boolean retValZero, String... args) throws Exception { + int retVal; + File outfile = new File("output.log"); + String[] command = getCommand(args); + try { + retVal = execute(outfile, command); + } catch (Exception ex) { + if (outfile.exists()) { + System.err.println(Files.readString(outfile.toPath())); + } + throw ex; + } + + String output = Files.readString(outfile.toPath()); + if (retValZero) { + if (retVal != 0) { + System.err.println("command run:"); + for (String s : command) { System.err.println(s); } + System.err.println("command output:"); + System.err.println(output); + throw new AssertionError("jpackage exited with error: " + retVal); + } + } else { + if (retVal == 0) { + System.err.println(output); + throw new AssertionError("jpackage exited without error: " + retVal); + } + } + + if (VERBOSE) { + System.out.println("output ="); + System.out.println(output); + } + + return output; + } + + public static String executeToolProvider(boolean retValZero, String... args) throws Exception { + StringWriter writer = new StringWriter(); + PrintWriter pw = new PrintWriter(writer); + int retVal = JPACKAGE_TOOL.run(pw, pw, args); + String output = writer.toString(); + + if (retValZero) { + if (retVal != 0) { + System.err.println(output); + throw new AssertionError("jpackage exited with error: " + retVal); + } + } else { + if (retVal == 0) { + System.err.println(output); + throw new AssertionError("jpackage exited without error"); + } + } + + if (VERBOSE) { + System.out.println("output ="); + System.out.println(output); + } + + return output; + } + + public static boolean isWindows() { + return (OS.contains("win")); + } + + public static boolean isOSX() { + return (OS.contains("mac")); + } + + public static boolean isLinux() { + return ((OS.contains("nix") || OS.contains("nux"))); + } + + public static void createHelloImageJar(String inputDir) throws Exception { + createJar(false, "Hello", "image", inputDir); + } + + public static void createHelloImageJar() throws Exception { + createJar(false, "Hello", "image", "input"); + } + + public static void createHelloImageJarWithMainClass() throws Exception { + createJar(true, "Hello", "image", "input"); + } + + public static void createHelloInstallerJar() throws Exception { + createJar(false, "Hello", "installer", "input"); + } + + public static void createHelloInstallerJarWithMainClass() throws Exception { + createJar(true, "Hello", "installer", "input"); + } + + private static void createJar(boolean mainClassAttribute, String name, + String testType, String inputDir) throws Exception { + int retVal; + + File input = new File(inputDir); + if (!input.exists()) { + input.mkdirs(); + } + + Path src = Path.of(TEST_SRC_ROOT + File.separator + "apps" + + File.separator + testType + File.separator + name + ".java"); + Path dst = Path.of(name + ".java"); + + if (dst.toFile().exists()) { + Files.delete(dst); + } + Files.copy(src, dst); + + + File javacLog = new File("javac.log"); + try { + retVal = execute(javacLog, JAVAC.toString(), name + ".java"); + } catch (Exception ex) { + if (javacLog.exists()) { + System.err.println(Files.readString(javacLog.toPath())); + } + throw ex; + } + + if (retVal != 0) { + if (javacLog.exists()) { + System.err.println(Files.readString(javacLog.toPath())); + } + throw new AssertionError("javac exited with error: " + retVal); + } + + File jarLog = new File("jar.log"); + try { + List args = new ArrayList<>(); + args.add(JAR.toString()); + args.add("-c"); + args.add("-v"); + args.add("-f"); + args.add(inputDir + File.separator + name.toLowerCase() + ".jar"); + if (mainClassAttribute) { + args.add("-e"); + args.add(name); + } + args.add(name + ".class"); + retVal = execute(jarLog, args.stream().toArray(String[]::new)); + } catch (Exception ex) { + if (jarLog.exists()) { + System.err.println(Files.readString(jarLog.toPath())); + } + throw ex; + } + + if (retVal != 0) { + if (jarLog.exists()) { + System.err.println(Files.readString(jarLog.toPath())); + } + throw new AssertionError("jar exited with error: " + retVal); + } + } + + public static void createHelloModule() throws Exception { + createModule("Hello.java", "input", "hello", null, true); + } + + public static void createHelloModule(ModuleArgs moduleArgs) throws Exception { + createModule("Hello.java", "input", "hello", moduleArgs, true); + } + + public static void createOtherModule() throws Exception { + createModule("Other.java", "input-other", "other", null, false); + } + + private static void createModule(String javaFile, String inputDir, String aName, + ModuleArgs moduleArgs, boolean createModularJar) throws Exception { + int retVal; + + File input = new File(inputDir); + if (!input.exists()) { + input.mkdir(); + } + + File module = new File("module" + File.separator + "com." + aName); + if (!module.exists()) { + module.mkdirs(); + } + + File javacLog = new File("javac.log"); + try { + List args = new ArrayList<>(); + args.add(JAVAC.toString()); + args.add("-d"); + args.add("module" + File.separator + "com." + aName); + args.add(TEST_SRC_ROOT + File.separator + "apps" + File.separator + + "com." + aName + File.separator + "module-info.java"); + args.add(TEST_SRC_ROOT + File.separator + "apps" + + File.separator + "com." + aName + File.separator + "com" + + File.separator + aName + File.separator + javaFile); + retVal = execute(javacLog, args.stream().toArray(String[]::new)); + } catch (Exception ex) { + if (javacLog.exists()) { + System.err.println(Files.readString(javacLog.toPath())); + } + throw ex; + } + + if (retVal != 0) { + if (javacLog.exists()) { + System.err.println(Files.readString(javacLog.toPath())); + } + throw new AssertionError("javac exited with error: " + retVal); + } + + if (createModularJar) { + File jarLog = new File("jar.log"); + try { + List args = new ArrayList<>(); + args.add(JAR.toString()); + args.add("--create"); + args.add("--file"); + args.add(inputDir + File.separator + "com." + aName + ".jar"); + if (moduleArgs != null) { + if (moduleArgs.getVersion() != null) { + args.add("--module-version"); + args.add(moduleArgs.getVersion()); + } + + if (moduleArgs.getMainClass()!= null) { + args.add("--main-class"); + args.add(moduleArgs.getMainClass()); + } + } + args.add("-C"); + args.add("module" + File.separator + "com." + aName); + args.add("."); + + retVal = execute(jarLog, args.stream().toArray(String[]::new)); + } catch (Exception ex) { + if (jarLog.exists()) { + System.err.println(Files.readString(jarLog.toPath())); + } + throw ex; + } + + if (retVal != 0) { + if (jarLog.exists()) { + System.err.println(Files.readString(jarLog.toPath())); + } + throw new AssertionError("jar exited with error: " + retVal); + } + } + } + + public static void createRuntime() throws Exception { + List moreArgs = new ArrayList<>(); + createRuntime(moreArgs); + } + + public static void createRuntime(List moreArgs) throws Exception { + int retVal; + + File jlinkLog = new File("jlink.log"); + try { + List args = new ArrayList<>(); + args.add(JLINK.toString()); + args.add("--output"); + args.add("runtime"); + args.add("--add-modules"); + args.add("java.base"); + args.addAll(moreArgs); + + retVal = execute(jlinkLog, args.stream().toArray(String[]::new)); + } catch (Exception ex) { + if (jlinkLog.exists()) { + System.err.println(Files.readString(jlinkLog.toPath())); + } + throw ex; + } + + if (retVal != 0) { + if (jlinkLog.exists()) { + System.err.println(Files.readString(jlinkLog.toPath())); + } + throw new AssertionError("jlink exited with error: " + retVal); + } + } + + public static String listToArgumentsMap(List arguments, boolean toolProvider) { + if (arguments.isEmpty()) { + return ""; + } + + String argsStr = ""; + for (int i = 0; i < arguments.size(); i++) { + String arg = arguments.get(i); + argsStr += quote(arg, toolProvider); + if ((i + 1) != arguments.size()) { + argsStr += " "; + } + } + + if (!toolProvider && isWindows()) { + if (argsStr.contains(" ")) { + if (argsStr.contains("\"")) { + argsStr = escapeQuote(argsStr, toolProvider); + } + argsStr = "\"" + argsStr + "\""; + } + } + return argsStr; + } + + public static String[] cmdWithAtFilename(String [] cmd, int ndx, int len) + throws IOException { + ArrayList newAList = new ArrayList<>(); + String fileString = null; + for (int i=0; i ndx && i < ndx + len) { + fileString += " " + cmd[i]; + } else { + newAList.add(cmd[i]); + } + } + if (fileString != null) { + Path path = new File("argfile.cmds").toPath(); + try (BufferedWriter bw = Files.newBufferedWriter(path); + PrintWriter out = new PrintWriter(bw)) { + out.println(fileString); + } + } + return newAList.toArray(new String[0]); + } + + public static String [] splitAndFilter(String output) { + if (output == null) { + return null; + } + + return Stream.of(output.split("\\R")) + .filter(str -> !str.startsWith("Picked up")) + .filter(str -> !str.startsWith("WARNING: Using incubator")) + .filter(str -> !str.startsWith("hello: ")) + .collect(Collectors.toList()).toArray(String[]::new); + } + + private static String quote(String in, boolean toolProvider) { + if (in == null) { + return null; + } + + if (in.isEmpty()) { + return ""; + } + + if (!in.contains("=")) { + // Not a property + if (in.contains(" ")) { + in = escapeQuote(in, toolProvider); + return "\"" + in + "\""; + } + return in; + } + + if (!in.contains(" ")) { + return in; // No need to quote + } + + int paramIndex = in.indexOf("="); + if (paramIndex <= 0) { + return in; // Something wrong, just skip quoting + } + + String param = in.substring(0, paramIndex); + String value = in.substring(paramIndex + 1); + + if (value.length() == 0) { + return in; // No need to quote + } + + value = escapeQuote(value, toolProvider); + + return param + "=" + "\"" + value + "\""; + } + + private static String escapeQuote(String in, boolean toolProvider) { + if (in == null) { + return null; + } + + if (in.isEmpty()) { + return ""; + } + + if (in.contains("\"")) { + // Use code points to preserve non-ASCII chars + StringBuilder sb = new StringBuilder(); + int codeLen = in.codePointCount(0, in.length()); + for (int i = 0; i < codeLen; i++) { + int code = in.codePointAt(i); + // Note: No need to escape '\' on Linux or OS X + // jpackage expects us to pass arguments and properties with + // quotes and spaces as a map + // with quotes being escaped with additional \ for + // internal quotes. + // So if we want two properties below: + // -Djnlp.Prop1=Some "Value" 1 + // -Djnlp.Prop2=Some Value 2 + // jpackage will need: + // "-Djnlp.Prop1=\"Some \\"Value\\" 1\" -Djnlp.Prop2=\"Some Value 2\"" + // but since we using ProcessBuilder to run jpackage we will need to escape + // our escape symbols as well, so we will need to pass string below to ProcessBuilder: + // "-Djnlp.Prop1=\\\"Some \\\\\\\"Value\\\\\\\" 1\\\" -Djnlp.Prop2=\\\"Some Value 2\\\"" + switch (code) { + case '"': + // " -> \" -> \\\" + if (i == 0 || in.codePointAt(i - 1) != '\\') { + sb.appendCodePoint('\\'); + sb.appendCodePoint(code); + } + break; + case '\\': + // We need to escape already escaped symbols as well + if ((i + 1) < codeLen) { + int nextCode = in.codePointAt(i + 1); + if (nextCode == '"') { + // \" -> \\\" + sb.appendCodePoint('\\'); + sb.appendCodePoint('\\'); + sb.appendCodePoint('\\'); + sb.appendCodePoint(nextCode); + } else { + sb.appendCodePoint('\\'); + sb.appendCodePoint(code); + } + } else { + sb.appendCodePoint(code); + } + break; + default: + sb.appendCodePoint(code); + break; + } + } + return sb.toString(); + } + + return in; + } +} diff --git a/test/jdk/tools/jpackage/helpers/JPackageInstallerHelper.java b/test/jdk/tools/jpackage/helpers/JPackageInstallerHelper.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/JPackageInstallerHelper.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; + +public class JPackageInstallerHelper { + private static final String JPACKAGE_TEST_OUTPUT = "jpackage.test.output"; + private static final String JPACKAGE_VERIFY_INSTALL = "jpackage.verify.install"; + private static final String JPACKAGE_VERIFY_UNINSTALL = "jpackage.verify.uninstall"; + private static String testOutput; + private static final boolean isTestOutputSet; + private static final boolean isVerifyInstall; + private static final boolean isVerifyUnInstall; + + static { + String out = System.getProperty(JPACKAGE_TEST_OUTPUT); + isTestOutputSet = (out != null); + if (isTestOutputSet) { + File file = new File(out); + if (!file.exists()) { + throw new AssertionError(file.getAbsolutePath() + " does not exist"); + } + + if (!file.isDirectory()) { + throw new AssertionError(file.getAbsolutePath() + " is not a directory"); + } + + if (!file.canWrite()) { + throw new AssertionError(file.getAbsolutePath() + " is not writable"); + } + + if (out.endsWith(File.separator)) { + out = out.substring(0, out.length() - 2); + } + + testOutput = out; + } + + isVerifyInstall = (System.getProperty(JPACKAGE_VERIFY_INSTALL) != null); + isVerifyUnInstall = (System.getProperty(JPACKAGE_VERIFY_UNINSTALL) != null); + } + + public static boolean isTestOutputSet() { + return isTestOutputSet; + } + + public static boolean isVerifyInstall() { + return isVerifyInstall; + } + + public static boolean isVerifyUnInstall() { + return isVerifyUnInstall; + } + + public static void copyTestResults(List files) throws Exception { + if (!isTestOutputSet()) { + return; + } + + File dest = new File(testOutput); + if (!dest.exists()) { + dest.mkdirs(); + } + + if (JPackageHelper.isWindows()) { + files.add(JPackagePath.getTestSrc() + File.separator + "install.bat"); + files.add(JPackagePath.getTestSrc() + File.separator + "uninstall.bat"); + } else { + files.add(JPackagePath.getTestSrc() + File.separator + "install.sh"); + files.add(JPackagePath.getTestSrc() + File.separator + "uninstall.sh"); + } + + for (String file : files) { + Path source = Path.of(file); + Path target = Path.of(dest.toPath() + File.separator + source.getFileName()); + Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); + } + } + + public static void validateApp(String app) throws Exception { + File outFile = new File("appOutput.txt"); + if (outFile.exists()) { + outFile.delete(); + } + + int retVal = JPackageHelper.execute(outFile, app); + if (retVal != 0) { + throw new AssertionError( + "Test application exited with error: " + retVal); + } + + if (!outFile.exists()) { + throw new AssertionError(outFile.getAbsolutePath() + " was not created"); + } + + String output = Files.readString(outFile.toPath()); + String[] result = output.split("\n"); + if (result.length != 2) { + System.err.println(output); + throw new AssertionError( + "Unexpected number of lines: " + result.length); + } + + if (!result[0].trim().equals("jpackage test application")) { + throw new AssertionError("Unexpected result[0]: " + result[0]); + } + + if (!result[1].trim().equals("args.length: 0")) { + throw new AssertionError("Unexpected result[1]: " + result[1]); + } + } + + public static void validateOutput(String output) throws Exception { + File file = new File(output); + if (!file.exists()) { + // Try lower case in case of OS is case sensitive + file = new File(output.toLowerCase()); + if (!file.exists()) { + throw new AssertionError("Cannot find " + file.getAbsolutePath()); + } + } + } +} diff --git a/test/jdk/tools/jpackage/helpers/JPackagePath.java b/test/jdk/tools/jpackage/helpers/JPackagePath.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/JPackagePath.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.File; +import java.nio.file.Path; + +/** + * Helper class which contains functions to get different system + * dependent paths used by tests + */ +public class JPackagePath { + + // Return path to test src adjusted to location of caller + public static String getTestSrcRoot() { + return JPackageHelper.TEST_SRC_ROOT; + } + + // Return path to calling test + public static String getTestSrc() { + return JPackageHelper.TEST_SRC; + } + + // Returns path to generate test application + public static String getApp() { + return getApp("test"); + } + + public static String getApp(String name) { + return getAppSL(name, name); + } + + // Returns path to generate test application icon + public static String getAppIcon() { + return getAppIcon("test"); + } + + public static String getAppIcon(String name) { + if (JPackageHelper.isWindows()) { + return Path.of("output", name, name + ".ico").toString(); + } else if (JPackageHelper.isOSX()) { + return Path.of("output", name + ".app", + "Contents", "Resources", name + ".icns").toString(); + } else if (JPackageHelper.isLinux()) { + return Path.of("output", name, "lib", name + ".png").toString(); + } else { + throw new AssertionError("Cannot detect platform"); + } + } + + // Returns path to generate secondary launcher of given application + public static String getAppSL(String sl) { + return getAppSL("test", sl); + } + + public static String getAppSL(String app, String sl) { + if (JPackageHelper.isWindows()) { + return Path.of("output", app, sl + ".exe").toString(); + } else if (JPackageHelper.isOSX()) { + return Path.of("output", app + ".app", + "Contents", "MacOS", sl).toString(); + } else if (JPackageHelper.isLinux()) { + return Path.of("output", app, "bin", sl).toString(); + } else { + throw new AssertionError("Cannot detect platform"); + } + } + + // Returns path to test application cfg file + public static String getAppCfg() { + return getAppCfg("test"); + } + + public static String getAppCfg(String name) { + if (JPackageHelper.isWindows()) { + return Path.of("output", name, "app", name + ".cfg").toString(); + } else if (JPackageHelper.isOSX()) { + return Path.of("output", name + ".app", + "Contents", "app", name + ".cfg").toString(); + } else if (JPackageHelper.isLinux()) { + return Path.of("output", name, "lib", "app", name + ".cfg").toString(); + } else { + throw new AssertionError("Cannot detect platform"); + } + } + + // Returns path including executable to java in image runtime folder + public static String getRuntimeJava() { + return getRuntimeJava("test"); + } + + public static String getRuntimeJava(String name) { + if (JPackageHelper.isWindows()) { + return Path.of(getRuntimeBin(name), "java.exe").toString(); + } + return Path.of(getRuntimeBin(name), "java").toString(); + } + + // Returns output file name generate by test application + public static String getAppOutputFile() { + return "appOutput.txt"; + } + + // Returns path to bin folder in image runtime + public static String getRuntimeBin() { + return getRuntimeBin("test"); + } + + public static String getRuntimeBin(String name) { + if (JPackageHelper.isWindows()) { + return Path.of("output", name, "runtime", "bin").toString(); + } else if (JPackageHelper.isOSX()) { + return Path.of("output", name + ".app", + "Contents", "runtime", + "Contents", "Home", "bin").toString(); + } else if (JPackageHelper.isLinux()) { + return Path.of("output", name, "lib", "runtime", "bin").toString(); + } else { + throw new AssertionError("Cannot detect platform"); + } + } + + public static String getOSXInstalledApp(String testName) { + return File.separator + "Applications" + + File.separator + testName + ".app" + + File.separator + "Contents" + + File.separator + "MacOS" + + File.separator + testName; + } + + public static String getOSXInstalledApp(String subDir, String testName) { + return File.separator + "Applications" + + File.separator + subDir + + File.separator + testName + ".app" + + File.separator + "Contents" + + File.separator + "MacOS" + + File.separator + testName; + } + + // Returs path to test license file + public static String getLicenseFilePath() { + String path = JPackagePath.getTestSrcRoot() + + File.separator + "resources" + + File.separator + "license.txt"; + + return path; + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Annotations.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Annotations.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Annotations.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public class Annotations { + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface BeforeEach { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface AfterEach { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface Test { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @Repeatable(ParameterGroup.class) + public @interface Parameter { + + String[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface ParameterGroup { + + Parameter[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface Parameters { + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public final class CfgFile { + public String getValue(String section, String key) { + Objects.requireNonNull(section); + Objects.requireNonNull(key); + + Map entries = data.get(section); + TKit.assertTrue(entries != null, String.format( + "Check section [%s] is found in [%s] cfg file", section, id)); + + String value = entries.get(key); + TKit.assertNotNull(value, String.format( + "Check key [%s] is found in [%s] section of [%s] cfg file", key, + section, id)); + + return value; + } + + private CfgFile(Map> data, String id) { + this.data = data; + this.id = id; + } + + public static CfgFile readFromFile(Path path) throws IOException { + TKit.trace(String.format("Read [%s] jpackage cfg file", path)); + + final Pattern sectionBeginRegex = Pattern.compile( "\\s*\\[([^]]*)\\]\\s*"); + final Pattern keyRegex = Pattern.compile( "\\s*([^=]*)=(.*)" ); + + Map> result = new HashMap<>(); + + String currentSectionName = null; + Map currentSection = new HashMap<>(); + for (String line : Files.readAllLines(path)) { + Matcher matcher = sectionBeginRegex.matcher(line); + if (matcher.find()) { + if (currentSectionName != null) { + result.put(currentSectionName, Collections.unmodifiableMap( + new HashMap<>(currentSection))); + } + currentSectionName = matcher.group(1); + currentSection.clear(); + continue; + } + + matcher = keyRegex.matcher(line); + if (matcher.find()) { + currentSection.put(matcher.group(1), matcher.group(2)); + continue; + } + } + + if (!currentSection.isEmpty()) { + result.put("", Collections.unmodifiableMap(currentSection)); + } + + return new CfgFile(Collections.unmodifiableMap(result), path.toString()); + } + + private final Map> data; + private final String id; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CommandArguments.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CommandArguments.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CommandArguments.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class CommandArguments { + + CommandArguments() { + args = new ArrayList<>(); + } + + final public T addArgument(String v) { + args.add(v); + return (T) this; + } + + final public T addArguments(List v) { + args.addAll(v); + return (T) this; + } + + final public T addArgument(Path v) { + return addArgument(v.toString()); + } + + final public T addArguments(String... v) { + return addArguments(Arrays.asList(v)); + } + + final public T addPathArguments(List v) { + return addArguments(v.stream().map((p) -> p.toString()).collect( + Collectors.toList())); + } + + final public List getAllArguments() { + return List.copyOf(args); + } + + protected void verifyMutable() { + if (!isMutable()) { + throw new UnsupportedOperationException( + "Attempt to modify immutable object"); + } + } + + protected boolean isMutable() { + return true; + } + + protected List args; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.StringReader; +import java.nio.file.Path; +import java.util.*; +import java.util.regex.Pattern; +import java.util.spi.ToolProvider; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.test.Functional.ThrowingSupplier; + +public final class Executor extends CommandArguments { + + public Executor() { + saveOutputType = new HashSet<>(Set.of(SaveOutputType.NONE)); + } + + public Executor setExecutable(String v) { + return setExecutable(Path.of(v)); + } + + public Executor setExecutable(Path v) { + executable = Objects.requireNonNull(v); + toolProvider = null; + return this; + } + + public Executor setToolProvider(ToolProvider v) { + toolProvider = Objects.requireNonNull(v); + executable = null; + return this; + } + + public Executor setToolProvider(JavaTool v) { + return setToolProvider(v.asToolProvider()); + } + + public Executor setDirectory(Path v) { + directory = v; + return this; + } + + public Executor setExecutable(JavaTool v) { + return setExecutable(v.getPath()); + } + + /** + * Configures this instance to save full output that command will produce. + * This function is mutual exclusive with + * saveFirstLineOfOutput() function. + * + * @return this + */ + public Executor saveOutput() { + saveOutputType.remove(SaveOutputType.FIRST_LINE); + saveOutputType.add(SaveOutputType.FULL); + return this; + } + + /** + * Configures how to save output that command will produce. If + * v is true, the function call is equivalent to + * saveOutput() call. If v is false, + * the function will result in not preserving command output. + * + * @return this + */ + public Executor saveOutput(boolean v) { + if (v) { + saveOutput(); + } else { + saveOutputType.remove(SaveOutputType.FIRST_LINE); + saveOutputType.remove(SaveOutputType.FULL); + } + return this; + } + + /** + * Configures this instance to save only the first line out output that + * command will produce. This function is mutual exclusive with + * saveOutput() function. + * + * @return this + */ + public Executor saveFirstLineOfOutput() { + saveOutputType.add(SaveOutputType.FIRST_LINE); + saveOutputType.remove(SaveOutputType.FULL); + return this; + } + + /** + * Configures this instance to dump all output that command will produce to + * System.out and System.err. Can be used together with saveOutput() and + * saveFirstLineOfOutput() to save command output and also copy it in the + * default output streams. + * + * @return this + */ + public Executor dumpOutput() { + return dumpOutput(true); + } + + public Executor dumpOutput(boolean v) { + if (v) { + saveOutputType.add(SaveOutputType.DUMP); + } else { + saveOutputType.remove(SaveOutputType.DUMP); + } + return this; + } + + public class Result { + + Result(int exitCode) { + this.exitCode = exitCode; + } + + public String getFirstLineOfOutput() { + return output.get(0); + } + + public List getOutput() { + return output; + } + + public String getPrintableCommandLine() { + return Executor.this.getPrintableCommandLine(); + } + + public Result assertExitCodeIs(int expectedExitCode) { + TKit.assertEquals(expectedExitCode, exitCode, String.format( + "Check command %s exited with %d code", + getPrintableCommandLine(), expectedExitCode)); + return this; + } + + public Result assertExitCodeIsZero() { + return assertExitCodeIs(0); + } + + final int exitCode; + private List output; + } + + public Result execute() { + if (toolProvider != null && directory != null) { + throw new IllegalArgumentException( + "Can't change directory when using tool provider"); + } + + return ThrowingSupplier.toSupplier(() -> { + if (toolProvider != null) { + return runToolProvider(); + } + + if (executable != null) { + return runExecutable(); + } + + throw new IllegalStateException("No command to execute"); + }).get(); + } + + public String executeAndGetFirstLineOfOutput() { + return saveFirstLineOfOutput().execute().assertExitCodeIsZero().getFirstLineOfOutput(); + } + + public List executeAndGetOutput() { + return saveOutput().execute().assertExitCodeIsZero().getOutput(); + } + + private boolean withSavedOutput() { + return saveOutputType.contains(SaveOutputType.FULL) || saveOutputType.contains( + SaveOutputType.FIRST_LINE); + } + + private Path executablePath() { + if (directory == null || executable.isAbsolute()) { + return executable; + } + + // If relative path to executable is used it seems to be broken when + // ProcessBuilder changes the directory. On Windows it changes the + // directory first and on Linux it looks up for executable before + // changing the directory. So to stay of safe side, use absolute path + // to executable. + return executable.toAbsolutePath(); + } + + private Result runExecutable() throws IOException, InterruptedException { + List command = new ArrayList<>(); + command.add(executablePath().toString()); + command.addAll(args); + ProcessBuilder builder = new ProcessBuilder(command); + StringBuilder sb = new StringBuilder(getPrintableCommandLine()); + if (withSavedOutput()) { + builder.redirectErrorStream(true); + sb.append("; save output"); + } else if (saveOutputType.contains(SaveOutputType.DUMP)) { + builder.inheritIO(); + sb.append("; inherit I/O"); + } else { + builder.redirectError(ProcessBuilder.Redirect.DISCARD); + builder.redirectOutput(ProcessBuilder.Redirect.DISCARD); + sb.append("; discard I/O"); + } + if (directory != null) { + builder.directory(directory.toFile()); + sb.append(String.format("; in directory [%s]", directory)); + } + + TKit.trace("Execute " + sb.toString() + "..."); + Process process = builder.start(); + + List outputLines = null; + if (withSavedOutput()) { + try (BufferedReader outReader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + if (saveOutputType.contains(SaveOutputType.DUMP) + || saveOutputType.contains(SaveOutputType.FULL)) { + outputLines = outReader.lines().collect(Collectors.toList()); + } else { + outputLines = Arrays.asList( + outReader.lines().findFirst().orElse(null)); + } + } finally { + if (saveOutputType.contains(SaveOutputType.DUMP) && outputLines != null) { + outputLines.stream().forEach(System.out::println); + if (saveOutputType.contains(SaveOutputType.FIRST_LINE)) { + // Pick the first line of saved output if there is one + for (String line: outputLines) { + outputLines = List.of(line); + break; + } + } + } + } + } + + Result reply = new Result(process.waitFor()); + TKit.trace("Done. Exit code: " + reply.exitCode); + + if (outputLines != null) { + reply.output = Collections.unmodifiableList(outputLines); + } + return reply; + } + + private Result runToolProvider(PrintStream out, PrintStream err) { + TKit.trace("Execute " + getPrintableCommandLine() + "..."); + Result reply = new Result(toolProvider.run(out, err, args.toArray( + String[]::new))); + TKit.trace("Done. Exit code: " + reply.exitCode); + return reply; + } + + + private Result runToolProvider() throws IOException { + if (!withSavedOutput()) { + if (saveOutputType.contains(SaveOutputType.DUMP)) { + return runToolProvider(System.out, System.err); + } + + PrintStream nullPrintStream = new PrintStream(new OutputStream() { + @Override + public void write(int b) { + // Nop + } + }); + return runToolProvider(nullPrintStream, nullPrintStream); + } + + try (ByteArrayOutputStream buf = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(buf)) { + Result reply = runToolProvider(ps, ps); + ps.flush(); + try (BufferedReader bufReader = new BufferedReader(new StringReader( + buf.toString()))) { + if (saveOutputType.contains(SaveOutputType.FIRST_LINE)) { + String firstLine = bufReader.lines().findFirst().orElse(null); + if (firstLine != null) { + reply.output = List.of(firstLine); + } + } else if (saveOutputType.contains(SaveOutputType.FULL)) { + reply.output = bufReader.lines().collect( + Collectors.toUnmodifiableList()); + } + + if (saveOutputType.contains(SaveOutputType.DUMP)) { + Stream lines; + if (saveOutputType.contains(SaveOutputType.FULL)) { + lines = reply.output.stream(); + } else { + lines = bufReader.lines(); + } + lines.forEach(System.out::println); + } + } + return reply; + } + } + + public String getPrintableCommandLine() { + final String exec; + String format = "[%s](%d)"; + if (toolProvider == null && executable == null) { + exec = ""; + } else if (toolProvider != null) { + format = "tool provider " + format; + exec = toolProvider.name(); + } else { + exec = executablePath().toString(); + } + + return String.format(format, printCommandLine(exec, args), + args.size() + 1); + } + + private static String printCommandLine(String executable, List args) { + // Want command line printed in a way it can be easily copy/pasted + // to be executed manally + Pattern regex = Pattern.compile("\\s"); + return Stream.concat(Stream.of(executable), args.stream()).map( + v -> (v.isEmpty() || regex.matcher(v).find()) ? "\"" + v + "\"" : v).collect( + Collectors.joining(" ")); + } + + private ToolProvider toolProvider; + private Path executable; + private Set saveOutputType; + private Path directory; + + private static enum SaveOutputType { + NONE, FULL, FIRST_LINE, DUMP + }; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + + +final public class FileAssociations { + public FileAssociations(String faSuffixName) { + suffixName = faSuffixName; + setFilename("fa"); + setDescription("jpackage test extention"); + } + + private void createFile() { + Map entries = new HashMap<>(Map.of( + "extension", suffixName, + "mime-type", getMime(), + "description", description + )); + if (icon != null) { + if (TKit.isWindows()) { + entries.put("icon", icon.toString().replace("\\", "/")); + } else { + entries.put("icon", icon.toString()); + } + } + TKit.createPropertiesFile(file, entries); + } + + public FileAssociations setFilename(String v) { + file = TKit.workDir().resolve(v + ".properties"); + return this; + } + + public FileAssociations setDescription(String v) { + description = v; + return this; + } + + public FileAssociations setIcon(Path v) { + icon = v; + return this; + } + + Path getPropertiesFile() { + return file; + } + + String getSuffix() { + return "." + suffixName; + } + + String getMime() { + return "application/x-jpackage-" + suffixName; + } + + public void applyTo(PackageTest test) { + test.notForTypes(PackageType.MAC_DMG, () -> { + test.addInitializer(cmd -> { + createFile(); + cmd.addArguments("--file-associations", getPropertiesFile()); + }); + test.addHelloAppFileAssociationsVerifier(this); + }); + } + + private Path file; + final private String suffixName; + private String description; + private Path icon; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Functional.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Functional.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Functional.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.lang.reflect.InvocationTargetException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + + +public class Functional { + @FunctionalInterface + public interface ThrowingConsumer { + void accept(T t) throws Throwable; + + public static Consumer toConsumer(ThrowingConsumer v) { + return o -> { + try { + v.accept(o); + } catch (Throwable ex) { + rethrowUnchecked(ex); + } + }; + } + } + + @FunctionalInterface + public interface ThrowingSupplier { + T get() throws Throwable; + + public static Supplier toSupplier(ThrowingSupplier v) { + return () -> { + try { + return v.get(); + } catch (Throwable ex) { + rethrowUnchecked(ex); + } + // Unreachable + return null; + }; + } + } + + @FunctionalInterface + public interface ThrowingFunction { + R apply(T t) throws Throwable; + + public static Function toFunction(ThrowingFunction v) { + return (t) -> { + try { + return v.apply(t); + } catch (Throwable ex) { + rethrowUnchecked(ex); + } + // Unreachable + return null; + }; + } + } + + @FunctionalInterface + public interface ThrowingRunnable { + void run() throws Throwable; + + public static Runnable toRunnable(ThrowingRunnable v) { + return () -> { + try { + v.run(); + } catch (Throwable ex) { + rethrowUnchecked(ex); + } + }; + } + } + + public static Supplier identity(Supplier v) { + return v; + } + + public static Consumer identity(Consumer v) { + return v; + } + + public static Runnable identity(Runnable v) { + return v; + } + + public static Function identity(Function v) { + return v; + } + + public static Function identityFunction(Function v) { + return v; + } + + public static Predicate identity(Predicate v) { + return v; + } + + public static Predicate identityPredicate(Predicate v) { + return v; + } + + public static class ExceptionBox extends RuntimeException { + public ExceptionBox(Throwable throwable) { + super(throwable); + } + } + + @SuppressWarnings("unchecked") + public static void rethrowUnchecked(Throwable throwable) throws ExceptionBox { + if (throwable instanceof ExceptionBox) { + throw (ExceptionBox)throwable; + } + + if (throwable instanceof InvocationTargetException) { + new ExceptionBox(throwable.getCause()); + } + + throw new ExceptionBox(throwable); + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import jdk.jpackage.test.Functional.ThrowingFunction; +import jdk.jpackage.test.Functional.ThrowingSupplier; + +public class HelloApp { + + HelloApp(JavaAppDesc appDesc) { + if (appDesc == null) { + this.appDesc = createDefaltAppDesc(); + } else { + this.appDesc = appDesc; + } + } + + private JarBuilder prepareSources(Path srcDir) throws IOException { + final String qualifiedClassName = appDesc.className(); + + final String className = qualifiedClassName.substring( + qualifiedClassName.lastIndexOf('.') + 1); + final String packageName = appDesc.packageName(); + + final Path srcFile = srcDir.resolve(Path.of(String.join( + File.separator, qualifiedClassName.split("\\.")) + ".java")); + Files.createDirectories(srcFile.getParent()); + + JarBuilder jarBuilder = createJarBuilder().addSourceFile(srcFile); + final String moduleName = appDesc.moduleName(); + if (moduleName != null) { + Path moduleInfoFile = srcDir.resolve("module-info.java"); + TKit.createTextFile(moduleInfoFile, List.of( + String.format("module %s {", moduleName), + String.format(" exports %s;", packageName), + " requires java.desktop;", + "}" + )); + jarBuilder.addSourceFile(moduleInfoFile); + jarBuilder.setModuleVersion(appDesc.moduleVersion()); + } + + // Add package directive and replace class name in java source file. + // Works with simple test Hello.java. + // Don't expect too much from these regexps! + Pattern classNameRegex = Pattern.compile("\\bHello\\b"); + Pattern classDeclaration = Pattern.compile( + "(^.*\\bclass\\s+)\\bHello\\b(.*$)"); + Pattern importDirective = Pattern.compile( + "(?<=import (?:static )?+)[^;]+"); + AtomicBoolean classDeclared = new AtomicBoolean(); + AtomicBoolean packageInserted = new AtomicBoolean(packageName == null); + + var packageInserter = Functional.identityFunction((line) -> { + packageInserted.setPlain(true); + return String.format("package %s;%s%s", packageName, + System.lineSeparator(), line); + }); + + Files.write(srcFile, Files.readAllLines(HELLO_JAVA).stream().map(line -> { + Matcher m; + if (classDeclared.getPlain()) { + if ((m = classNameRegex.matcher(line)).find()) { + line = m.replaceAll(className); + } + return line; + } + + if (!packageInserted.getPlain() && importDirective.matcher(line).find()) { + line = packageInserter.apply(line); + } else if ((m = classDeclaration.matcher(line)).find()) { + classDeclared.setPlain(true); + line = m.group(1) + className + m.group(2); + if (!packageInserted.getPlain()) { + line = packageInserter.apply(line); + } + } + return line; + }).collect(Collectors.toList())); + + return jarBuilder; + } + + private JarBuilder createJarBuilder() { + JarBuilder builder = new JarBuilder(); + if (appDesc.jarWithMainClass()) { + builder.setMainClass(appDesc.className()); + } + return builder; + } + + void addTo(JPackageCommand cmd) { + final String moduleName = appDesc.moduleName(); + final String jarFileName = appDesc.jarFileName(); + final String qualifiedClassName = appDesc.className(); + + if (moduleName != null && appDesc.packageName() == null) { + throw new IllegalArgumentException(String.format( + "Module [%s] with default package", moduleName)); + } + + if (moduleName == null && CLASS_NAME.equals(qualifiedClassName)) { + // Use Hello.java as is. + cmd.addAction((self) -> { + Path jarFile = self.inputDir().resolve(jarFileName); + createJarBuilder().setOutputJar(jarFile).addSourceFile( + HELLO_JAVA).create(); + }); + } else { + cmd.addAction((self) -> { + final Path jarFile; + if (moduleName == null) { + jarFile = self.inputDir().resolve(jarFileName); + } else { + // `--module-path` option should be set by the moment + // when this action is being executed. + jarFile = Path.of(self.getArgumentValue("--module-path", + () -> self.inputDir().toString()), jarFileName); + Files.createDirectories(jarFile.getParent()); + } + + TKit.withTempDirectory("src", + workDir -> prepareSources(workDir).setOutputJar(jarFile).create()); + }); + } + + if (moduleName == null) { + cmd.addArguments("--main-jar", jarFileName); + cmd.addArguments("--main-class", qualifiedClassName); + } else { + cmd.addArguments("--module-path", TKit.workDir().resolve( + "input-modules")); + cmd.addArguments("--module", String.join("/", moduleName, + qualifiedClassName)); + // For modular app assume nothing will go in input directory and thus + // nobody will create input directory, so remove corresponding option + // from jpackage command line. + cmd.removeArgumentWithValue("--input"); + } + if (TKit.isWindows()) { + cmd.addArguments("--win-console"); + } + } + + static JavaAppDesc createDefaltAppDesc() { + return new JavaAppDesc().setClassName(CLASS_NAME).setJarFileName( + "hello.jar"); + } + + static void verifyOutputFile(Path outputFile, List args) { + if (!outputFile.isAbsolute()) { + verifyOutputFile(outputFile.toAbsolutePath().normalize(), args); + return; + } + + TKit.assertFileExists(outputFile); + + List contents = ThrowingSupplier.toSupplier( + () -> Files.readAllLines(outputFile)).get(); + + List expected = new ArrayList<>(List.of( + "jpackage test application", + String.format("args.length: %d", args.size()) + )); + expected.addAll(args); + + TKit.assertStringListEquals(expected, contents, String.format( + "Check contents of [%s] file", outputFile)); + } + + public static void executeLauncherAndVerifyOutput(JPackageCommand cmd) { + final Path launcherPath = cmd.appLauncherPath(); + if (!cmd.isFakeRuntime(String.format("Not running [%s] launcher", + launcherPath))) { + executeAndVerifyOutput(launcherPath, cmd.getAllArgumentValues( + "--arguments")); + } + } + + public static void executeAndVerifyOutput(Path helloAppLauncher, + String... defaultLauncherArgs) { + executeAndVerifyOutput(helloAppLauncher, List.of(defaultLauncherArgs)); + } + + public static void executeAndVerifyOutput(Path helloAppLauncher, + List defaultLauncherArgs) { + // Output file will be created in the current directory. + Path outputFile = TKit.workDir().resolve(OUTPUT_FILENAME); + ThrowingFunction.toFunction(Files::deleteIfExists).apply(outputFile); + new Executor() + .setDirectory(outputFile.getParent()) + .setExecutable(helloAppLauncher) + .dumpOutput() + .execute() + .assertExitCodeIsZero(); + + verifyOutputFile(outputFile, defaultLauncherArgs); + } + + final static String OUTPUT_FILENAME = "appOutput.txt"; + + private final JavaAppDesc appDesc; + + private static final Path HELLO_JAVA = TKit.TEST_SRC_ROOT.resolve( + "apps/image/Hello.java"); + + private final static String CLASS_NAME = HELLO_JAVA.getFileName().toString().split( + "\\.", 2)[0]; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -0,0 +1,732 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.SecureRandom; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.incubator.jpackage.internal.ApplicationLayout; +import jdk.jpackage.test.Functional.ThrowingConsumer; +import jdk.jpackage.test.Functional.ThrowingFunction; + +/** + * jpackage command line with prerequisite actions. Prerequisite actions can be + * anything. The simplest is to compile test application and pack in a jar for + * use on jpackage command line. + */ +public final class JPackageCommand extends CommandArguments { + + public JPackageCommand() { + actions = new ArrayList<>(); + } + + public JPackageCommand(JPackageCommand cmd) { + this(); + args.addAll(cmd.args); + withToolProvider = cmd.withToolProvider; + saveConsoleOutput = cmd.saveConsoleOutput; + suppressOutput = cmd.suppressOutput; + ignoreDefaultRuntime = cmd.ignoreDefaultRuntime; + immutable = cmd.immutable; + actionsExecuted = cmd.actionsExecuted; + } + + JPackageCommand createImmutableCopy() { + JPackageCommand reply = new JPackageCommand(this); + reply.immutable = true; + return reply; + } + + public JPackageCommand setArgumentValue(String argName, String newValue) { + verifyMutable(); + + String prevArg = null; + ListIterator it = args.listIterator(); + while (it.hasNext()) { + String value = it.next(); + if (prevArg != null && prevArg.equals(argName)) { + if (newValue != null) { + it.set(newValue); + } else { + it.remove(); + it.previous(); + it.remove(); + } + return this; + } + prevArg = value; + } + + if (newValue != null) { + addArguments(argName, newValue); + } + + return this; + } + + public JPackageCommand setArgumentValue(String argName, Path newValue) { + return setArgumentValue(argName, newValue.toString()); + } + + public JPackageCommand removeArgumentWithValue(String argName) { + return setArgumentValue(argName, (String)null); + } + + public JPackageCommand removeArgument(String argName) { + args = args.stream().filter(arg -> !arg.equals(argName)).collect( + Collectors.toList()); + return this; + } + + public boolean hasArgument(String argName) { + return args.contains(argName); + } + + public T getArgumentValue(String argName, + Function defaultValueSupplier, + Function stringConverter) { + String prevArg = null; + for (String arg : args) { + if (prevArg != null && prevArg.equals(argName)) { + return stringConverter.apply(arg); + } + prevArg = arg; + } + if (defaultValueSupplier != null) { + return defaultValueSupplier.apply(this); + } + return null; + } + + public String getArgumentValue(String argName, + Function defaultValueSupplier) { + return getArgumentValue(argName, defaultValueSupplier, v -> v); + } + + public T getArgumentValue(String argName, + Supplier defaultValueSupplier, + Function stringConverter) { + return getArgumentValue(argName, (unused) -> defaultValueSupplier.get(), + stringConverter); + } + + public String getArgumentValue(String argName, + Supplier defaultValueSupplier) { + return getArgumentValue(argName, defaultValueSupplier, v -> v); + } + + public String getArgumentValue(String argName) { + return getArgumentValue(argName, (Supplier)null); + } + + public String[] getAllArgumentValues(String argName) { + List values = new ArrayList<>(); + String prevArg = null; + for (String arg : args) { + if (prevArg != null && prevArg.equals(argName)) { + values.add(arg); + } + prevArg = arg; + } + return values.toArray(String[]::new); + } + + public JPackageCommand addArguments(String name, Path value) { + return addArguments(name, value.toString()); + } + + public boolean isImagePackageType() { + return PackageType.IMAGE == getArgumentValue("--type", + () -> null, PACKAGE_TYPES::get); + } + + public PackageType packageType() { + // Don't try to be in sync with jpackage defaults. Keep it simple: + // if no `--type` explicitely set on the command line, consider + // this is operator's fault. + return getArgumentValue("--type", + () -> { + throw new IllegalStateException("Package type not set"); + }, PACKAGE_TYPES::get); + } + + public Path outputDir() { + return getArgumentValue("--dest", () -> Path.of("."), Path::of); + } + + public Path inputDir() { + return getArgumentValue("--input", () -> null, Path::of); + } + + public String version() { + return getArgumentValue("--app-version", () -> "1.0"); + } + + public String name() { + return getArgumentValue("--name", () -> getArgumentValue("--main-class")); + } + + public boolean isRuntime() { + return hasArgument("--runtime-image") + && !hasArgument("--main-jar") + && !hasArgument("--module") + && !hasArgument("--app-image"); + } + + public JPackageCommand setDefaultInputOutput() { + addArguments("--input", TKit.defaultInputDir()); + addArguments("--dest", TKit.defaultOutputDir()); + return this; + } + + public JPackageCommand setFakeRuntime() { + verifyMutable(); + + ThrowingConsumer createBulkFile = path -> { + Files.createDirectories(path.getParent()); + try (FileOutputStream out = new FileOutputStream(path.toFile())) { + byte[] bytes = new byte[4 * 1024]; + new SecureRandom().nextBytes(bytes); + out.write(bytes); + } + }; + + addAction(cmd -> { + Path fakeRuntimeDir = TKit.workDir().resolve("fake_runtime"); + + TKit.trace(String.format("Init fake runtime in [%s] directory", + fakeRuntimeDir)); + + Files.createDirectories(fakeRuntimeDir); + + if (TKit.isWindows() || TKit.isLinux()) { + // Needed to make WindowsAppBundler happy as it copies MSVC dlls + // from `bin` directory. + // Need to make the code in rpm spec happy as it assumes there is + // always something in application image. + fakeRuntimeDir.resolve("bin").toFile().mkdir(); + } + + if (TKit.isOSX()) { + // Make MacAppImageBuilder happy + createBulkFile.accept(fakeRuntimeDir.resolve(Path.of( + "Contents/Home/lib/jli/libjli.dylib"))); + } + + // Mak sure fake runtime takes some disk space. + // Package bundles with 0KB size are unexpected and considered + // an error by PackageTest. + createBulkFile.accept(fakeRuntimeDir.resolve(Path.of("bin", "bulk"))); + + cmd.addArguments("--runtime-image", fakeRuntimeDir); + }); + + return this; + } + + JPackageCommand addAction(ThrowingConsumer action) { + verifyMutable(); + actions.add(ThrowingConsumer.toConsumer(action)); + return this; + } + + /** + * Shorthand for {@code helloAppImage(null)}. + */ + public static JPackageCommand helloAppImage() { + JavaAppDesc javaAppDesc = null; + return helloAppImage(javaAppDesc); + } + + /** + * Creates new JPackageCommand instance configured with the test Java app. + * For the explanation of `javaAppDesc` parameter, see documentation for + * #JavaAppDesc.parse() method. + * + * @param javaAppDesc Java application description + * @return this + */ + public static JPackageCommand helloAppImage(String javaAppDesc) { + final JavaAppDesc appDesc; + if (javaAppDesc == null) { + appDesc = null; + } else { + appDesc = JavaAppDesc.parse(javaAppDesc); + } + return helloAppImage(appDesc); + } + + public static JPackageCommand helloAppImage(JavaAppDesc javaAppDesc) { + JPackageCommand cmd = new JPackageCommand(); + cmd.setDefaultInputOutput().setDefaultAppName(); + PackageType.IMAGE.applyTo(cmd); + new HelloApp(javaAppDesc).addTo(cmd); + return cmd; + } + + public JPackageCommand setPackageType(PackageType type) { + verifyMutable(); + type.applyTo(this); + return this; + } + + JPackageCommand setDefaultAppName() { + return addArguments("--name", TKit.getCurrentDefaultAppName()); + } + + /** + * Returns path to output bundle of configured jpackage command. + * + * If this is build image command, returns path to application image directory. + */ + public Path outputBundle() { + final String bundleName; + if (isImagePackageType()) { + String dirName = name(); + if (TKit.isOSX()) { + dirName = dirName + ".app"; + } + bundleName = dirName; + } else if (TKit.isLinux()) { + bundleName = LinuxHelper.getBundleName(this); + } else if (TKit.isWindows()) { + bundleName = WindowsHelper.getBundleName(this); + } else if (TKit.isOSX()) { + bundleName = MacHelper.getBundleName(this); + } else { + throw TKit.throwUnknownPlatformError(); + } + + return outputDir().resolve(bundleName); + } + + /** + * Returns application layout. + * + * If this is build image command, returns application image layout of the + * output bundle relative to output directory. Otherwise returns layout of + * installed application relative to the root directory. + * + * If this command builds Java runtime, not an application, returns + * corresponding layout. + */ + public ApplicationLayout appLayout() { + final ApplicationLayout layout; + if (isRuntime()) { + layout = ApplicationLayout.javaRuntime(); + } else { + layout = ApplicationLayout.platformAppImage(); + } + + if (isImagePackageType()) { + return layout.resolveAt(outputBundle()); + } + + return layout.resolveAt(appInstallationDirectory()); + } + + /** + * Returns path to directory where application will be installed or null if + * this is build image command. + * + * E.g. on Linux for app named Foo default the function will return + * `/opt/foo` + */ + public Path appInstallationDirectory() { + if (isImagePackageType()) { + return null; + } + + if (TKit.isLinux()) { + if (isRuntime()) { + // Not fancy, but OK. + return Path.of(getArgumentValue("--install-dir", () -> "/opt"), + LinuxHelper.getPackageName(this)); + } + + // Launcher is in "bin" subfolder of the installation directory. + return appLauncherPath().getParent().getParent(); + } + + if (TKit.isWindows()) { + return WindowsHelper.getInstallationDirectory(this); + } + + if (TKit.isOSX()) { + return MacHelper.getInstallationDirectory(this); + } + + throw TKit.throwUnknownPlatformError(); + } + + /** + * Returns path to application's Java runtime. + * If the command will package Java runtime only, returns correct path to + * runtime directory. + * + * E.g.: + * [jpackage --name Foo --type rpm] -> `/opt/foo/lib/runtime` + * [jpackage --name Foo --type app-image --dest bar] -> `bar/Foo/lib/runtime` + * [jpackage --name Foo --type rpm --runtime-image java] -> `/opt/foo` + */ + public Path appRuntimeDirectory() { + return appLayout().runtimeDirectory(); + } + + /** + * Returns path for application launcher with the given name. + * + * E.g.: [jpackage --name Foo --type rpm] -> `/opt/foo/bin/Foo` + * [jpackage --name Foo --type app-image --dest bar] -> + * `bar/Foo/bin/Foo` + * + * @param launcherName name of launcher or {@code null} for the main + * launcher + * + * @throws IllegalArgumentException if the command is configured for + * packaging Java runtime + */ + public Path appLauncherPath(String launcherName) { + verifyNotRuntime(); + if (launcherName == null) { + launcherName = name(); + } + + if (TKit.isWindows()) { + launcherName = launcherName + ".exe"; + } + + if (isImagePackageType()) { + return appLayout().launchersDirectory().resolve(launcherName); + } + + if (TKit.isLinux()) { + return LinuxHelper.getLauncherPath(this).getParent().resolve(launcherName); + } + + return appLayout().launchersDirectory().resolve(launcherName); + } + + /** + * Shorthand for {@code appLauncherPath(null)}. + */ + public Path appLauncherPath() { + return appLauncherPath(null); + } + + private void verifyNotRuntime() { + if (isRuntime()) { + throw new IllegalArgumentException("Java runtime packaging"); + } + } + + /** + * Returns path to .cfg file of the given application launcher. + * + * E.g.: + * [jpackage --name Foo --type rpm] -> `/opt/foo/lib/app/Foo.cfg` + * [jpackage --name Foo --type app-image --dest bar] -> `bar/Foo/lib/app/Foo.cfg` + * + * @param launcher name of launcher or {@code null} for the main launcher + * + * @throws IllegalArgumentException if the command is configured for + * packaging Java runtime + */ + public Path appLauncherCfgPath(String launcherName) { + verifyNotRuntime(); + if (launcherName == null) { + launcherName = name(); + } + return appLayout().appDirectory().resolve(launcherName + ".cfg"); + } + + public boolean isFakeRuntime(String msg) { + Path runtimeDir = appRuntimeDirectory(); + + final Collection criticalRuntimeFiles; + if (TKit.isWindows()) { + criticalRuntimeFiles = WindowsHelper.CRITICAL_RUNTIME_FILES; + } else if (TKit.isLinux()) { + criticalRuntimeFiles = LinuxHelper.CRITICAL_RUNTIME_FILES; + } else if (TKit.isOSX()) { + criticalRuntimeFiles = MacHelper.CRITICAL_RUNTIME_FILES; + } else { + throw TKit.throwUnknownPlatformError(); + } + + if (criticalRuntimeFiles.stream().filter( + v -> runtimeDir.resolve(v).toFile().exists()).findFirst().orElse( + null) == null) { + // Fake runtime + TKit.trace(String.format( + "%s because application runtime directory [%s] is incomplete", + msg, runtimeDir)); + return true; + } + return false; + } + + public static void useToolProviderByDefault() { + defaultWithToolProvider = true; + } + + public static void useExecutableByDefault() { + defaultWithToolProvider = false; + } + + public JPackageCommand useToolProvider(boolean v) { + verifyMutable(); + withToolProvider = v; + return this; + } + + public JPackageCommand saveConsoleOutput(boolean v) { + verifyMutable(); + saveConsoleOutput = v; + return this; + } + + public JPackageCommand dumpOutput(boolean v) { + verifyMutable(); + suppressOutput = !v; + return this; + } + + public JPackageCommand ignoreDefaultRuntime(boolean v) { + verifyMutable(); + ignoreDefaultRuntime = v; + return this; + } + + public boolean isWithToolProvider() { + return Optional.ofNullable(withToolProvider).orElse( + defaultWithToolProvider); + } + + public JPackageCommand executePrerequisiteActions() { + verifyMutable(); + if (!actionsExecuted) { + actionsExecuted = true; + if (actions != null) { + actions.stream().forEach(r -> r.accept(this)); + } + } + return this; + } + + public Executor createExecutor() { + verifyMutable(); + Executor exec = new Executor() + .saveOutput(saveConsoleOutput).dumpOutput(!suppressOutput) + .addArguments(args); + + if (isWithToolProvider()) { + exec.setToolProvider(JavaTool.JPACKAGE); + } else { + exec.setExecutable(JavaTool.JPACKAGE); + } + + return exec; + } + + public Executor.Result execute() { + executePrerequisiteActions(); + + if (isImagePackageType()) { + TKit.deleteDirectoryContentsRecursive(outputDir()); + } + + return new JPackageCommand(this) + .adjustArgumentsBeforeExecution() + .createExecutor() + .execute(); + } + + public JPackageCommand executeAndAssertHelloAppImageCreated() { + executeAndAssertImageCreated(); + HelloApp.executeLauncherAndVerifyOutput(this); + return this; + } + + public JPackageCommand executeAndAssertImageCreated() { + execute().assertExitCodeIsZero(); + return assertImageCreated(); + } + + public JPackageCommand assertImageCreated() { + verifyIsOfType(PackageType.IMAGE); + TKit.assertDirectoryExists(appRuntimeDirectory()); + + if (!isRuntime()) { + TKit.assertExecutableFileExists(appLauncherPath()); + TKit.assertFileExists(appLauncherCfgPath(null)); + } + + return this; + } + + private JPackageCommand adjustArgumentsBeforeExecution() { + if (!hasArgument("--runtime-image") && !hasArgument("--app-image") && DEFAULT_RUNTIME_IMAGE != null && !ignoreDefaultRuntime) { + addArguments("--runtime-image", DEFAULT_RUNTIME_IMAGE); + } + + if (!hasArgument("--verbose") && TKit.VERBOSE_JPACKAGE) { + addArgument("--verbose"); + } + + return this; + } + + String getPrintableCommandLine() { + return new Executor() + .setExecutable(JavaTool.JPACKAGE) + .addArguments(args) + .getPrintableCommandLine(); + } + + public void verifyIsOfType(Collection types) { + verifyIsOfType(types.toArray(PackageType[]::new)); + } + + public void verifyIsOfType(PackageType ... types) { + final var typesSet = Stream.of(types).collect(Collectors.toSet()); + if (!hasArgument("--type")) { + if (!isImagePackageType()) { + if (TKit.isLinux() && typesSet.equals(PackageType.LINUX)) { + return; + } + + if (TKit.isWindows() && typesSet.equals(PackageType.WINDOWS)) { + return; + } + + if (TKit.isOSX() && typesSet.equals(PackageType.MAC)) { + return; + } + } else if (typesSet.equals(Set.of(PackageType.IMAGE))) { + return; + } + } + + if (!typesSet.contains(packageType())) { + throw new IllegalArgumentException("Unexpected type"); + } + } + + public CfgFile readLaunherCfgFile() { + return readLaunherCfgFile(null); + } + + public CfgFile readLaunherCfgFile(String launcherName) { + verifyIsOfType(PackageType.IMAGE); + if (isRuntime()) { + return null; + } + return ThrowingFunction.toFunction(CfgFile::readFromFile).apply( + appLauncherCfgPath(launcherName)); + } + + public static String escapeAndJoin(String... args) { + return escapeAndJoin(List.of(args)); + } + + public static String escapeAndJoin(List args) { + Pattern whitespaceRegexp = Pattern.compile("\\s"); + + return args.stream().map(v -> { + String str = v; + // Escape quotes. + str = str.replace("\"", "\\\""); + // Escape backslashes. + str = str.replace("\\", "\\\\"); + // If value contains whitespace characters, put the value in quotes + if (whitespaceRegexp.matcher(str).find()) { + str = "\"" + str + "\""; + } + return str; + }).collect(Collectors.joining(" ")); + } + + public static Path relativePathInRuntime(JavaTool tool) { + Path path = tool.relativePathInJavaHome(); + if (TKit.isOSX()) { + path = Path.of("Contents/Home").resolve(path); + } + return path; + } + + public static Stream filterOutput(Stream jpackageOutput) { + // Skip "WARNING: Using incubator ..." first line of output + return jpackageOutput.skip(1); + } + + public static List filterOutput(List jpackageOutput) { + return filterOutput(jpackageOutput.stream()).collect(Collectors.toList()); + } + + @Override + protected boolean isMutable() { + return !immutable; + } + + private Boolean withToolProvider; + private boolean saveConsoleOutput; + private boolean suppressOutput; + private boolean ignoreDefaultRuntime; + private boolean immutable; + private boolean actionsExecuted; + private final List> actions; + private static boolean defaultWithToolProvider; + + private final static Map PACKAGE_TYPES = Functional.identity( + () -> { + Map reply = new HashMap<>(); + for (PackageType type : PackageType.values()) { + reply.put(type.getName(), type); + } + return reply; + }).get(); + + public final static Path DEFAULT_RUNTIME_IMAGE = Functional.identity(() -> { + // Set the property to the path of run-time image to speed up + // building app images and platform bundles by avoiding running jlink + // The value of the property will be automativcally appended to + // jpackage command line if the command line doesn't have + // `--runtime-image` parameter set. + String val = TKit.getConfigProperty("runtime-image"); + if (val != null) { + return Path.of(val); + } + return null; + }).get(); +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.test; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import java.util.ArrayList; +import java.util.List; + + +/** + * Tool to compile Java sources and pack in a jar file. + */ +public final class JarBuilder { + + public JarBuilder() { + sourceFiles = new ArrayList<>(); + } + + public JarBuilder setOutputJar(Path v) { + outputJar = v; + return this; + } + + public JarBuilder setMainClass(String v) { + mainClass = v; + return this; + } + + public JarBuilder addSourceFile(Path v) { + sourceFiles.add(v); + return this; + } + + public JarBuilder setModuleVersion(String v) { + moduleVersion = v; + return this; + } + + public void create() { + TKit.withTempDirectory("jar-workdir", workDir -> { + if (!sourceFiles.isEmpty()) { + new Executor() + .setToolProvider(JavaTool.JAVAC) + .addArguments("-d", workDir.toString()) + .addPathArguments(sourceFiles) + .execute().assertExitCodeIsZero(); + } + + Files.createDirectories(outputJar.getParent()); + if (Files.exists(outputJar)) { + TKit.trace(String.format("Delete [%s] existing jar file", outputJar)); + Files.deleteIfExists(outputJar); + } + + Executor jarExe = new Executor() + .setToolProvider(JavaTool.JAR) + .addArguments("-c", "-f", outputJar.toString()); + if (moduleVersion != null) { + jarExe.addArguments(String.format("--module-version=%s", + moduleVersion)); + } + if (mainClass != null) { + jarExe.addArguments("-e", mainClass); + } + jarExe.addArguments("-C", workDir.toString(), "."); + jarExe.execute().assertExitCodeIsZero(); + }); + } + private List sourceFiles; + private Path outputJar; + private String mainClass; + private String moduleVersion; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaAppDesc.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaAppDesc.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaAppDesc.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.io.File; +import java.nio.file.Path; + + +public final class JavaAppDesc { + public JavaAppDesc() { + } + + public JavaAppDesc setClassName(String v) { + qualifiedClassName = v; + return this; + } + + public JavaAppDesc setModuleName(String v) { + moduleName = v; + return this; + } + + public JavaAppDesc setJarFileName(String v) { + jarFileName = v; + return this; + } + + public JavaAppDesc setModuleVersion(String v) { + moduleVersion = v; + return this; + } + + public JavaAppDesc setJarWithMainClass(boolean v) { + jarWithMainClass = v; + return this; + } + + public String className() { + return qualifiedClassName; + } + + public Path classFilePath() { + return Path.of(qualifiedClassName.replace(".", File.separator) + + ".class"); + } + + public String moduleName() { + return moduleName; + } + + public String packageName() { + int lastDotIdx = qualifiedClassName.lastIndexOf('.'); + if (lastDotIdx == -1) { + return null; + } + return qualifiedClassName.substring(0, lastDotIdx); + } + + public String jarFileName() { + return jarFileName; + } + + public String moduleVersion() { + return moduleVersion; + } + + public boolean jarWithMainClass() { + return jarWithMainClass; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (jarFileName != null) { + sb.append(jarFileName).append(':'); + } + if (moduleName != null) { + sb.append(moduleName).append('/'); + } + if (qualifiedClassName != null) { + sb.append(qualifiedClassName); + } + if (jarWithMainClass) { + sb.append('!'); + } + if (moduleVersion != null) { + sb.append('@').append(moduleVersion); + } + return sb.toString(); + } + + /** + * Create Java application description form encoded string value. + * + * Syntax of encoded Java application description is + * [jar_file:][module_name/]qualified_class_name[!][@module_version]. + * + * E.g.: `duke.jar:com.other/com.other.foo.bar.Buz!@3.7` encodes modular + * application. Module name is `com.other`. Main class is + * `com.other.foo.bar.Buz`. Module version is `3.7`. Application will be + * compiled and packed in `duke.jar` jar file. jar command will set module + * version (3.7) and main class (Buz) attributes in the jar file. + * + * E.g.: `Ciao` encodes non-modular `Ciao` class in the default package. + * jar command will not put main class attribute in the jar file. + * Default name will be picked for jar file - `hello.jar`. + * + * @param cmd jpackage command to configure + * @param javaAppDesc encoded Java application description + */ + public static JavaAppDesc parse(String javaAppDesc) { + JavaAppDesc desc = HelloApp.createDefaltAppDesc(); + + if (javaAppDesc == null) { + return desc; + } + + String moduleNameAndOther = Functional.identity(() -> { + String[] components = javaAppDesc.split(":", 2); + if (components.length == 2) { + desc.setJarFileName(components[0]); + } + return components[components.length - 1]; + }).get(); + + String classNameAndOther = Functional.identity(() -> { + String[] components = moduleNameAndOther.split("/", 2); + if (components.length == 2) { + desc.setModuleName(components[0]); + } + return components[components.length - 1]; + }).get(); + + Functional.identity(() -> { + String[] components = classNameAndOther.split("@", 2); + if (components[0].endsWith("!")) { + components[0] = components[0].substring(0, + components[0].length() - 1); + desc.setJarWithMainClass(true); + } + desc.setClassName(components[0]); + if (components.length == 2) { + desc.setModuleVersion(components[1]); + } + }).run(); + + return desc; + } + + private String qualifiedClassName; + private String moduleName; + private String jarFileName; + private String moduleVersion; + private boolean jarWithMainClass; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaTool.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaTool.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaTool.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +package jdk.jpackage.test; + + +import java.nio.file.Path; +import java.util.spi.ToolProvider; + +public enum JavaTool { + JAVA("java"), JAVAC("javac"), JPACKAGE("jpackage"), JAR("jar"), JLINK("jlink"); + + JavaTool(String name) { + this.name = name; + this.path = Path.of(System.getProperty("java.home")).resolve( + relativePathInJavaHome()).toAbsolutePath().normalize(); + if (!path.toFile().exists()) { + throw new RuntimeException(String.format( + "Unable to find tool [%s] at path=[%s]", name, path)); + } + } + + Path getPath() { + return path; + } + + public ToolProvider asToolProvider() { + return ToolProvider.findFirst(name).orElse(null); + } + + Path relativePathInJavaHome() { + Path path = Path.of("bin", name); + if (TKit.isWindows()) { + path = path.getParent().resolve(path.getFileName().toString() + ".exe"); + } + return path; + } + + private Path path; + private String name; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -0,0 +1,408 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class LinuxHelper { + private static String getRelease(JPackageCommand cmd) { + return cmd.getArgumentValue("--linux-app-release", () -> "1"); + } + + public static String getPackageName(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX); + return cmd.getArgumentValue("--linux-package-name", + () -> cmd.name().toLowerCase()); + } + + static String getBundleName(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX); + + final PackageType packageType = cmd.packageType(); + String format = null; + switch (packageType) { + case LINUX_DEB: + format = "%s_%s-%s_%s"; + break; + + case LINUX_RPM: + format = "%s-%s-%s.%s"; + break; + } + + final String release = getRelease(cmd); + final String version = cmd.version(); + + return String.format(format, getPackageName(cmd), version, release, + getDefaultPackageArch(packageType)) + packageType.getSuffix(); + } + + public static Stream getPackageFiles(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX); + + final PackageType packageType = cmd.packageType(); + final Path packageFile = cmd.outputBundle(); + + Executor exec = new Executor(); + switch (packageType) { + case LINUX_DEB: + exec.setExecutable("dpkg") + .addArgument("--contents") + .addArgument(packageFile); + break; + + case LINUX_RPM: + exec.setExecutable("rpm") + .addArgument("-qpl") + .addArgument(packageFile); + break; + } + + Stream lines = exec.executeAndGetOutput().stream(); + if (packageType == PackageType.LINUX_DEB) { + // Typical text lines produced by dpkg look like: + // drwxr-xr-x root/root 0 2019-08-30 05:30 ./opt/appcategorytest/runtime/lib/ + // -rw-r--r-- root/root 574912 2019-08-30 05:30 ./opt/appcategorytest/runtime/lib/libmlib_image.so + // Need to skip all fields but absolute path to file. + lines = lines.map(line -> line.substring(line.indexOf(" ./") + 2)); + } + return lines.map(Path::of); + } + + public static List getPrerequisitePackages(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX); + var packageType = cmd.packageType(); + switch (packageType) { + case LINUX_DEB: + return Stream.of(getDebBundleProperty(cmd.outputBundle(), + "Depends").split(",")).map(String::strip).collect( + Collectors.toList()); + + case LINUX_RPM: + return new Executor().setExecutable("rpm") + .addArguments("-qp", "-R", cmd.outputBundle().toString()) + .executeAndGetOutput(); + } + // Unreachable + return null; + } + + public static String getBundleProperty(JPackageCommand cmd, + String propertyName) { + return getBundleProperty(cmd, + Map.of(PackageType.LINUX_DEB, propertyName, + PackageType.LINUX_RPM, propertyName)); + } + + public static String getBundleProperty(JPackageCommand cmd, + Map propertyName) { + cmd.verifyIsOfType(PackageType.LINUX); + var packageType = cmd.packageType(); + switch (packageType) { + case LINUX_DEB: + return getDebBundleProperty(cmd.outputBundle(), propertyName.get( + packageType)); + + case LINUX_RPM: + return getRpmBundleProperty(cmd.outputBundle(), propertyName.get( + packageType)); + } + // Unrechable + return null; + } + + static Path getLauncherPath(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX); + + final String launcherName = cmd.name(); + final String launcherRelativePath = Path.of("/bin", launcherName).toString(); + + return getPackageFiles(cmd).filter(path -> path.toString().endsWith( + launcherRelativePath)).findFirst().or(() -> { + TKit.assertUnexpected(String.format( + "Failed to find %s in %s package", launcherName, + getPackageName(cmd))); + return null; + }).get(); + } + + static long getInstalledPackageSizeKB(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX); + + final Path packageFile = cmd.outputBundle(); + switch (cmd.packageType()) { + case LINUX_DEB: + return Long.parseLong(getDebBundleProperty(packageFile, + "Installed-Size")); + + case LINUX_RPM: + return Long.parseLong(getRpmBundleProperty(packageFile, "Size")) >> 10; + } + + return 0; + } + + static String getDebBundleProperty(Path bundle, String fieldName) { + return new Executor() + .setExecutable("dpkg-deb") + .addArguments("-f", bundle.toString(), fieldName) + .executeAndGetFirstLineOfOutput(); + } + + static String getRpmBundleProperty(Path bundle, String fieldName) { + return new Executor() + .setExecutable("rpm") + .addArguments( + "-qp", + "--queryformat", + String.format("%%{%s}", fieldName), + bundle.toString()) + .executeAndGetFirstLineOfOutput(); + } + + static void verifyPackageBundleEssential(JPackageCommand cmd) { + String packageName = LinuxHelper.getPackageName(cmd); + TKit.assertNotEquals(0L, LinuxHelper.getInstalledPackageSizeKB( + cmd), String.format( + "Check installed size of [%s] package in KB is not zero", + packageName)); + + final boolean checkPrerequisites; + if (cmd.isRuntime()) { + Path runtimeDir = cmd.appRuntimeDirectory(); + Set expectedCriticalRuntimePaths = CRITICAL_RUNTIME_FILES.stream().map( + runtimeDir::resolve).collect(Collectors.toSet()); + Set actualCriticalRuntimePaths = getPackageFiles(cmd).filter( + expectedCriticalRuntimePaths::contains).collect( + Collectors.toSet()); + checkPrerequisites = expectedCriticalRuntimePaths.equals( + actualCriticalRuntimePaths); + } else { + checkPrerequisites = true; + } + + List prerequisites = LinuxHelper.getPrerequisitePackages(cmd); + if (checkPrerequisites) { + final String vitalPackage = "libc"; + TKit.assertTrue(prerequisites.stream().filter( + dep -> dep.contains(vitalPackage)).findAny().isPresent(), + String.format( + "Check [%s] package is in the list of required packages %s of [%s] package", + vitalPackage, prerequisites, packageName)); + } else { + TKit.trace(String.format( + "Not cheking %s required packages of [%s] package", + prerequisites, packageName)); + } + } + + static void addBundleDesktopIntegrationVerifier(PackageTest test, + boolean integrated) { + final String xdgUtils = "xdg-utils"; + + test.addBundleVerifier(cmd -> { + List prerequisites = getPrerequisitePackages(cmd); + boolean xdgUtilsFound = prerequisites.contains(xdgUtils); + if (integrated) { + TKit.assertTrue(xdgUtilsFound, String.format( + "Check [%s] is in the list of required packages %s", + xdgUtils, prerequisites)); + } else { + TKit.assertFalse(xdgUtilsFound, String.format( + "Check [%s] is NOT in the list of required packages %s", + xdgUtils, prerequisites)); + } + }); + + test.forTypes(PackageType.LINUX_DEB, () -> { + addDebBundleDesktopIntegrationVerifier(test, integrated); + }); + } + + private static void addDebBundleDesktopIntegrationVerifier(PackageTest test, + boolean integrated) { + Function, String> verifier = (lines) -> { + // Lookup for xdg commands + return lines.stream().filter(line -> { + Set words = Stream.of(line.split("\\s+")).collect( + Collectors.toSet()); + return words.contains("xdg-desktop-menu") || words.contains( + "xdg-mime") || words.contains("xdg-icon-resource"); + }).findFirst().orElse(null); + }; + + test.addBundleVerifier(cmd -> { + TKit.withTempDirectory("dpkg-control-files", tempDir -> { + // Extract control Debian package files into temporary directory + new Executor() + .setExecutable("dpkg") + .addArguments( + "-e", + cmd.outputBundle().toString(), + tempDir.toString() + ).execute().assertExitCodeIsZero(); + + Path controlFile = Path.of("postinst"); + + // Lookup for xdg commands in postinstall script + String lineWithXsdCommand = verifier.apply( + Files.readAllLines(tempDir.resolve(controlFile))); + String assertMsg = String.format( + "Check if %s@%s control file uses xdg commands", + cmd.outputBundle(), controlFile); + if (integrated) { + TKit.assertNotNull(lineWithXsdCommand, assertMsg); + } else { + TKit.assertNull(lineWithXsdCommand, assertMsg); + } + }); + }); + } + + static void initFileAssociationsTestFile(Path testFile) { + try { + // Write something in test file. + // On Ubuntu and Oracle Linux empty files are considered + // plain text. Seems like a system bug. + // + // $ >foo.jptest1 + // $ xdg-mime query filetype foo.jptest1 + // text/plain + // $ echo > foo.jptest1 + // $ xdg-mime query filetype foo.jptest1 + // application/x-jpackage-jptest1 + // + Files.write(testFile, Arrays.asList("")); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private static Path getSystemDesktopFilesFolder() { + return Stream.of("/usr/share/applications", + "/usr/local/share/applications").map(Path::of).filter(dir -> { + return Files.exists(dir.resolve("defaults.list")); + }).findFirst().orElseThrow(() -> new RuntimeException( + "Failed to locate system .desktop files folder")); + } + + static void addFileAssociationsVerifier(PackageTest test, FileAssociations fa) { + test.addInstallVerifier(cmd -> { + PackageTest.withTestFileAssociationsFile(fa, testFile -> { + String mimeType = queryFileMimeType(testFile); + + TKit.assertEquals(fa.getMime(), mimeType, String.format( + "Check mime type of [%s] file", testFile)); + + String desktopFileName = queryMimeTypeDefaultHandler(mimeType); + + Path desktopFile = getSystemDesktopFilesFolder().resolve( + desktopFileName); + + TKit.assertFileExists(desktopFile); + + TKit.trace(String.format("Reading [%s] file...", desktopFile)); + String mimeHandler = Files.readAllLines(desktopFile).stream().peek( + v -> TKit.trace(v)).filter( + v -> v.startsWith("Exec=")).map( + v -> v.split("=", 2)[1]).findFirst().orElseThrow(); + + TKit.trace(String.format("Done")); + + TKit.assertEquals(cmd.appLauncherPath().toString(), + mimeHandler, String.format( + "Check mime type handler is the main application launcher")); + + }); + }); + + test.addUninstallVerifier(cmd -> { + PackageTest.withTestFileAssociationsFile(fa, testFile -> { + String mimeType = queryFileMimeType(testFile); + + TKit.assertNotEquals(fa.getMime(), mimeType, String.format( + "Check mime type of [%s] file", testFile)); + + String desktopFileName = queryMimeTypeDefaultHandler(fa.getMime()); + + TKit.assertNull(desktopFileName, String.format( + "Check there is no default handler for [%s] mime type", + fa.getMime())); + }); + }); + } + + private static String queryFileMimeType(Path file) { + return new Executor() + .setExecutable("xdg-mime") + .addArguments("query", "filetype", file.toString()) + .executeAndGetFirstLineOfOutput(); + } + + private static String queryMimeTypeDefaultHandler(String mimeType) { + return new Executor() + .setExecutable("xdg-mime") + .addArguments("query", "default", mimeType) + .executeAndGetFirstLineOfOutput(); + } + + public static String getDefaultPackageArch(PackageType type) { + if (archs == null) { + archs = new HashMap<>(); + } + + String arch = archs.get(type); + if (arch == null) { + Executor exec = new Executor(); + switch (type) { + case LINUX_DEB: + exec.setExecutable("dpkg").addArgument( + "--print-architecture"); + break; + + case LINUX_RPM: + exec.setExecutable("rpmbuild").addArgument( + "--eval=%{_target_cpu}"); + break; + } + arch = exec.executeAndGetFirstLineOfOutput(); + archs.put(type, arch); + } + return arch; + } + + static final Set CRITICAL_RUNTIME_FILES = Set.of(Path.of( + "lib/server/libjvm.so")); + + static private Map archs; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import jdk.jpackage.test.Functional.ThrowingConsumer; +import jdk.jpackage.test.Functional.ThrowingSupplier; +import org.xml.sax.SAXException; + +public class MacHelper { + + public static void withExplodedDmg(JPackageCommand cmd, + ThrowingConsumer consumer) { + cmd.verifyIsOfType(PackageType.MAC_DMG); + + var plist = readPList(new Executor() + .setExecutable("/usr/bin/hdiutil") + .dumpOutput() + .addArguments("attach", cmd.outputBundle().toString(), "-plist") + .executeAndGetOutput()); + + final Path mountPoint = Path.of(plist.queryValue("mount-point")); + try { + Path dmgImage = mountPoint.resolve(cmd.name() + ".app"); + TKit.trace(String.format("Exploded [%s] in [%s] directory", + cmd.outputBundle(), dmgImage)); + ThrowingConsumer.toConsumer(consumer).accept(dmgImage); + } finally { + new Executor() + .setExecutable("/usr/bin/hdiutil") + .addArgument("detach").addArgument(mountPoint) + .execute().assertExitCodeIsZero(); + } + } + + public static PListWrapper readPListFromAppImage(Path appImage) { + return readPList(appImage.resolve("Contents/Info.plist")); + } + + public static PListWrapper readPList(Path path) { + TKit.assertReadableFileExists(path); + return ThrowingSupplier.toSupplier(() -> readPList(Files.readAllLines( + path))).get(); + } + + public static PListWrapper readPList(List lines) { + return readPList(lines.stream()); + } + + public static PListWrapper readPList(Stream lines) { + return ThrowingSupplier.toSupplier(() -> new PListWrapper(lines.collect( + Collectors.joining()))).get(); + } + + static String getBundleName(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.MAC); + return String.format("%s-%s%s", getPackageName(cmd), cmd.version(), + cmd.packageType().getSuffix()); + } + + static Path getInstallationDirectory(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.MAC); + return Path.of(cmd.getArgumentValue("--install-dir", () -> "/Applications")) + .resolve(cmd.name() + ".app"); + } + + private static String getPackageName(JPackageCommand cmd) { + return cmd.getArgumentValue("--mac-package-name", + () -> cmd.name()); + } + + public static final class PListWrapper { + public String queryValue(String keyName) { + XPath xPath = XPathFactory.newInstance().newXPath(); + // Query for the value of element preceding element + // with value equal to `keyName` + String query = String.format( + "//string[preceding-sibling::key = \"%s\"][1]", keyName); + return ThrowingSupplier.toSupplier(() -> (String) xPath.evaluate( + query, doc, XPathConstants.STRING)).get(); + } + + PListWrapper(String xml) throws ParserConfigurationException, + SAXException, IOException { + doc = createDocumentBuilder().parse(new ByteArrayInputStream( + xml.getBytes(StandardCharsets.UTF_8))); + } + + private static DocumentBuilder createDocumentBuilder() throws + ParserConfigurationException { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newDefaultInstance(); + dbf.setFeature( + "http://apache.org/xml/features/nonvalidating/load-external-dtd", + false); + return dbf.newDocumentBuilder(); + } + + private final org.w3c.dom.Document doc; + } + + static final Set CRITICAL_RUNTIME_FILES = Set.of(Path.of( + "Contents/Home/lib/server/libjvm.dylib")); + +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Main.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Main.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Main.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.test; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import static jdk.jpackage.test.TestBuilder.CMDLINE_ARG_PREFIX; + + +public final class Main { + public static void main(String args[]) throws Throwable { + boolean listTests = false; + List tests = new ArrayList<>(); + try (TestBuilder testBuilder = new TestBuilder(tests::add)) { + for (var arg : args) { + TestBuilder.trace(String.format("Parsing [%s]...", arg)); + + if ((CMDLINE_ARG_PREFIX + "list").equals(arg)) { + listTests = true; + continue; + } + + boolean success = false; + try { + testBuilder.processCmdLineArg(arg); + success = true; + } catch (Throwable throwable) { + TKit.unbox(throwable); + } finally { + if (!success) { + TKit.log( + String.format("Error processing parameter=[%s]", + arg)); + } + } + } + } + + // Order tests by their full names to have stable test sequence. + List orderedTests = tests.stream() + .sorted((a, b) -> a.fullName().compareTo(b.fullName())) + .collect(Collectors.toList()); + + if (listTests) { + // Just list the tests + orderedTests.stream().forEach(test -> System.out.println(String.format( + "%s; workDir=[%s]", test.fullName(), test.workDir()))); + return; + } + + TKit.withExtraLogStream(() -> runTests(orderedTests)); + } + + private static void runTests(List tests) { + TKit.runTests(tests); + + final long passedCount = tests.stream().filter(TestInstance::passed).count(); + TKit.log(String.format("[==========] %d tests ran", tests.size())); + TKit.log(String.format("[ PASSED ] %d %s", passedCount, + passedCount == 1 ? "test" : "tests")); + + reportDetails(tests, "[ SKIPPED ]", TestInstance::skipped, false); + reportDetails(tests, "[ FAILED ]", TestInstance::failed, true); + + var withSkipped = reportSummary(tests, "SKIPPED", TestInstance::skipped); + var withFailures = reportSummary(tests, "FAILED", TestInstance::failed); + + if (withFailures != null) { + throw withFailures; + } + + if (withSkipped != null) { + tests.stream().filter(TestInstance::skipped).findFirst().get().rethrowIfSkipped(); + } + } + + private static long reportDetails(List tests, + String label, Predicate selector, boolean printWorkDir) { + + final Function makeMessage = test -> { + if (printWorkDir) { + return String.format("%s %s; workDir=[%s]", label, + test.fullName(), test.workDir()); + } + return String.format("%s %s", label, test.fullName()); + }; + + final long count = tests.stream().filter(selector).count(); + if (count != 0) { + TKit.log(String.format("%s %d %s, listed below", label, count, count + == 1 ? "test" : "tests")); + tests.stream().filter(selector).map(makeMessage).forEachOrdered( + TKit::log); + } + + return count; + } + + private static RuntimeException reportSummary(List tests, + String label, Predicate selector) { + final long count = tests.stream().filter(selector).count(); + if (count != 0) { + final String message = String.format("%d %s %s", count, label, count + == 1 ? "TEST" : "TESTS"); + TKit.log(message); + return new RuntimeException(message); + } + + return null; + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MethodCall.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MethodCall.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MethodCall.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.test.Functional.ThrowingConsumer; +import jdk.jpackage.test.TestInstance.TestDesc; + +class MethodCall implements ThrowingConsumer { + + MethodCall(Object[] instanceCtorArgs, Method method) { + this.ctorArgs = Optional.ofNullable(instanceCtorArgs).orElse( + DEFAULT_CTOR_ARGS); + this.method = method; + this.methodArgs = new Object[0]; + } + + MethodCall(Object[] instanceCtorArgs, Method method, Object arg) { + this.ctorArgs = Optional.ofNullable(instanceCtorArgs).orElse( + DEFAULT_CTOR_ARGS); + this.method = method; + this.methodArgs = new Object[]{arg}; + } + + TestDesc createDescription() { + var descBuilder = TestDesc.createBuilder().method(method); + if (methodArgs.length != 0) { + descBuilder.methodArgs(methodArgs); + } + + if (ctorArgs.length != 0) { + descBuilder.ctorArgs(ctorArgs); + } + + return descBuilder.get(); + } + + Method getMethod() { + return method; + } + + Object newInstance() throws NoSuchMethodException, InstantiationException, + IllegalAccessException, IllegalArgumentException, + InvocationTargetException { + if ((method.getModifiers() & Modifier.STATIC) != 0) { + return null; + } + + Constructor ctor = findRequiredConstructor(method.getDeclaringClass(), + ctorArgs); + if (ctor.isVarArgs()) { + // Assume constructor doesn't have fixed, only variable parameters. + return ctor.newInstance(new Object[]{ctorArgs}); + } + + return ctor.newInstance(ctorArgs); + } + + void checkRequiredConstructor() throws NoSuchMethodException { + if ((method.getModifiers() & Modifier.STATIC) == 0) { + findRequiredConstructor(method.getDeclaringClass(), ctorArgs); + } + } + + private static Constructor findVarArgConstructor(Class type) { + return Stream.of(type.getConstructors()).filter( + Constructor::isVarArgs).findFirst().orElse(null); + } + + private Constructor findRequiredConstructor(Class type, Object... ctorArgs) + throws NoSuchMethodException { + + Supplier notFoundException = () -> { + return new NoSuchMethodException(String.format( + "No public contructor in %s for %s arguments", type, + Arrays.deepToString(ctorArgs))); + }; + + if (Stream.of(ctorArgs).allMatch(Objects::nonNull)) { + // No `null` in constructor args, take easy path + try { + return type.getConstructor(Stream.of(ctorArgs).map( + Object::getClass).collect(Collectors.toList()).toArray( + Class[]::new)); + } catch (NoSuchMethodException ex) { + // Failed to find ctor that can take the given arguments. + Constructor varArgCtor = findVarArgConstructor(type); + if (varArgCtor != null) { + // There is one with variable number of arguments. Use it. + return varArgCtor; + } + throw notFoundException.get(); + } + } + + List ctors = Stream.of(type.getConstructors()) + .filter(ctor -> ctor.getParameterCount() == ctorArgs.length) + .collect(Collectors.toList()); + + if (ctors.isEmpty()) { + // No public constructors that can handle the given arguments. + throw notFoundException.get(); + } + + if (ctors.size() == 1) { + return ctors.iterator().next(); + } + + // Revisit this tricky case when it will start bothering. + throw notFoundException.get(); + } + + @Override + public void accept(Object thiz) throws Throwable { + method.invoke(thiz, methodArgs); + } + + private final Object[] methodArgs; + private final Method method; + private final Object[] ctorArgs; + + final static Object[] DEFAULT_CTOR_ARGS = new Object[0]; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java @@ -0,0 +1,487 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.awt.Desktop; +import java.awt.GraphicsEnvironment; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.test.Functional.ThrowingConsumer; +import jdk.incubator.jpackage.internal.AppImageFile; +import static jdk.jpackage.test.PackageType.*; + +/** + * Instance of PackageTest is for configuring and running a single jpackage + * command to produce platform specific package bundle. + * + * Provides methods to hook up custom configuration of jpackage command and + * verification of the output bundle. + */ +public final class PackageTest { + + /** + * Default test configuration for jpackage command. Default jpackage command + * initialization includes: + *
  • Set --input and --dest parameters. + *
  • Set --name parameter. Value of the parameter is the name of the first + * class with main function found in the callers stack. Defaults can be + * overridden with custom initializers set with subsequent addInitializer() + * function calls. + */ + public PackageTest() { + action = DEFAULT_ACTION; + excludeTypes = new HashSet<>(); + forTypes(); + setExpectedExitCode(0); + handlers = new HashMap<>(); + namedInitializers = new HashSet<>(); + currentTypes.forEach(v -> handlers.put(v, new Handler(v))); + } + + public PackageTest excludeTypes(PackageType... types) { + excludeTypes.addAll(Stream.of(types).collect(Collectors.toSet())); + return forTypes(currentTypes); + } + + public PackageTest excludeTypes(Collection types) { + return excludeTypes(types.toArray(PackageType[]::new)); + } + + public PackageTest forTypes(PackageType... types) { + Collection newTypes; + if (types == null || types.length == 0) { + newTypes = PackageType.NATIVE; + } else { + newTypes = Stream.of(types).collect(Collectors.toSet()); + } + currentTypes = newTypes.stream() + .filter(PackageType::isSupported) + .filter(Predicate.not(excludeTypes::contains)) + .collect(Collectors.toUnmodifiableSet()); + return this; + } + + public PackageTest forTypes(Collection types) { + return forTypes(types.toArray(PackageType[]::new)); + } + + public PackageTest notForTypes(PackageType... types) { + return notForTypes(List.of(types)); + } + + public PackageTest notForTypes(Collection types) { + Set workset = new HashSet<>(currentTypes); + workset.removeAll(types); + return forTypes(workset); + } + + public PackageTest setExpectedExitCode(int v) { + expectedJPackageExitCode = v; + return this; + } + + private PackageTest addInitializer(ThrowingConsumer v, + String id) { + if (id != null) { + if (namedInitializers.contains(id)) { + return this; + } + + namedInitializers.add(id); + } + currentTypes.stream().forEach(type -> handlers.get(type).addInitializer( + ThrowingConsumer.toConsumer(v))); + return this; + } + + public PackageTest addInitializer(ThrowingConsumer v) { + return addInitializer(v, null); + } + + public PackageTest addBundleVerifier( + BiConsumer v) { + currentTypes.stream().forEach( + type -> handlers.get(type).addBundleVerifier(v)); + return this; + } + + public PackageTest addBundleVerifier(ThrowingConsumer v) { + return addBundleVerifier( + (cmd, unused) -> ThrowingConsumer.toConsumer(v).accept(cmd)); + } + + public PackageTest addBundlePropertyVerifier(String propertyName, + BiConsumer pred) { + return addBundleVerifier(cmd -> { + pred.accept(propertyName, + LinuxHelper.getBundleProperty(cmd, propertyName)); + }); + } + + public PackageTest addBundlePropertyVerifier(String propertyName, + String expectedPropertyValue) { + return addBundlePropertyVerifier(propertyName, (unused, v) -> { + TKit.assertEquals(expectedPropertyValue, v, String.format( + "Check value of %s property is [%s]", propertyName, v)); + }); + } + + public PackageTest addBundleDesktopIntegrationVerifier(boolean integrated) { + forTypes(LINUX, () -> { + LinuxHelper.addBundleDesktopIntegrationVerifier(this, integrated); + }); + return this; + } + + public PackageTest addInstallVerifier(ThrowingConsumer v) { + currentTypes.stream().forEach( + type -> handlers.get(type).addInstallVerifier( + ThrowingConsumer.toConsumer(v))); + return this; + } + + public PackageTest addUninstallVerifier(ThrowingConsumer v) { + currentTypes.stream().forEach( + type -> handlers.get(type).addUninstallVerifier( + ThrowingConsumer.toConsumer(v))); + return this; + } + + static void withTestFileAssociationsFile(FileAssociations fa, + ThrowingConsumer consumer) { + final String testFileDefaultName = String.join(".", "test", + fa.getSuffix()); + TKit.withTempFile(testFileDefaultName, fa.getSuffix(), testFile -> { + if (TKit.isLinux()) { + LinuxHelper.initFileAssociationsTestFile(testFile); + } + consumer.accept(testFile); + }); + } + + PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa, + String... faLauncherDefaultArgs) { + + // Setup test app to have valid jpackage command line before + // running check of type of environment. + addInitializer(cmd -> new HelloApp(null).addTo(cmd), "HelloApp"); + + String noActionMsg = "Not running file associations test"; + if (GraphicsEnvironment.isHeadless()) { + TKit.trace(String.format( + "%s because running in headless environment", noActionMsg)); + return this; + } + + addInstallVerifier(cmd -> { + if (cmd.isFakeRuntime(noActionMsg)) { + return; + } + + withTestFileAssociationsFile(fa, testFile -> { + testFile = testFile.toAbsolutePath().normalize(); + + final Path appOutput = testFile.getParent() + .resolve(HelloApp.OUTPUT_FILENAME); + Files.deleteIfExists(appOutput); + + TKit.trace(String.format("Use desktop to open [%s] file", + testFile)); + Desktop.getDesktop().open(testFile.toFile()); + TKit.waitForFileCreated(appOutput, 7); + + List expectedArgs = new ArrayList<>(List.of( + faLauncherDefaultArgs)); + expectedArgs.add(testFile.toString()); + + // Wait a little bit after file has been created to + // make sure there are no pending writes into it. + Thread.sleep(3000); + HelloApp.verifyOutputFile(appOutput, expectedArgs); + }); + }); + + forTypes(PackageType.LINUX, () -> { + LinuxHelper.addFileAssociationsVerifier(this, fa); + }); + + return this; + } + + PackageTest forTypes(Collection types, Runnable action) { + Set oldTypes = Set.of(currentTypes.toArray( + PackageType[]::new)); + try { + forTypes(types); + action.run(); + } finally { + forTypes(oldTypes); + } + return this; + } + + PackageTest forTypes(PackageType type, Runnable action) { + return forTypes(List.of(type), action); + } + + PackageTest notForTypes(Collection types, Runnable action) { + Set workset = new HashSet<>(currentTypes); + workset.removeAll(types); + return forTypes(workset, action); + } + + PackageTest notForTypes(PackageType type, Runnable action) { + return notForTypes(List.of(type), action); + } + + public PackageTest configureHelloApp() { + return configureHelloApp(null); + } + + public PackageTest configureHelloApp(String encodedName) { + addInitializer( + cmd -> new HelloApp(JavaAppDesc.parse(encodedName)).addTo(cmd)); + addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput); + return this; + } + + public void run() { + List supportedHandlers = handlers.values().stream() + .filter(entry -> !entry.isVoid()) + .collect(Collectors.toList()); + + if (supportedHandlers.isEmpty()) { + // No handlers with initializers found. Nothing to do. + return; + } + + Supplier initializer = new Supplier<>() { + @Override + public JPackageCommand get() { + JPackageCommand cmd = new JPackageCommand().setDefaultInputOutput(); + if (bundleOutputDir != null) { + cmd.setArgumentValue("--dest", bundleOutputDir.toString()); + } + cmd.setDefaultAppName(); + return cmd; + } + }; + + supportedHandlers.forEach(handler -> handler.accept(initializer.get())); + } + + public PackageTest setAction(Action value) { + action = value; + return this; + } + + public Action getAction() { + return action; + } + + private class Handler implements Consumer { + + Handler(PackageType type) { + if (!PackageType.NATIVE.contains(type)) { + throw new IllegalArgumentException( + "Attempt to configure a test for image packaging"); + } + this.type = type; + initializers = new ArrayList<>(); + bundleVerifiers = new ArrayList<>(); + installVerifiers = new ArrayList<>(); + uninstallVerifiers = new ArrayList<>(); + } + + boolean isVoid() { + return initializers.isEmpty(); + } + + void addInitializer(Consumer v) { + initializers.add(v); + } + + void addBundleVerifier(BiConsumer v) { + bundleVerifiers.add(v); + } + + void addInstallVerifier(Consumer v) { + installVerifiers.add(v); + } + + void addUninstallVerifier(Consumer v) { + uninstallVerifiers.add(v); + } + + @Override + public void accept(JPackageCommand cmd) { + type.applyTo(cmd); + + initializers.stream().forEach(v -> v.accept(cmd)); + cmd.executePrerequisiteActions(); + + switch (action) { + case CREATE: + Executor.Result result = cmd.execute(); + result.assertExitCodeIs(expectedJPackageExitCode); + if (expectedJPackageExitCode == 0) { + TKit.assertFileExists(cmd.outputBundle()); + } else { + TKit.assertPathExists(cmd.outputBundle(), false); + } + verifyPackageBundle(cmd.createImmutableCopy(), result); + break; + + case VERIFY_INSTALL: + if (expectedJPackageExitCode == 0) { + verifyPackageInstalled(cmd.createImmutableCopy()); + } + break; + + case VERIFY_UNINSTALL: + if (expectedJPackageExitCode == 0) { + verifyPackageUninstalled(cmd.createImmutableCopy()); + } + break; + } + } + + private void verifyPackageBundle(JPackageCommand cmd, + Executor.Result result) { + if (expectedJPackageExitCode == 0) { + if (PackageType.LINUX.contains(cmd.packageType())) { + LinuxHelper.verifyPackageBundleEssential(cmd); + } + } + bundleVerifiers.stream().forEach(v -> v.accept(cmd, result)); + } + + private void verifyPackageInstalled(JPackageCommand cmd) { + TKit.trace(String.format("Verify installed: %s", + cmd.getPrintableCommandLine())); + TKit.assertDirectoryExists(cmd.appRuntimeDirectory()); + if (!cmd.isRuntime()) { + TKit.assertExecutableFileExists(cmd.appLauncherPath()); + + if (PackageType.WINDOWS.contains(cmd.packageType())) { + new WindowsHelper.AppVerifier(cmd); + } + } + + TKit.assertPathExists(AppImageFile.getPathInAppImage( + cmd.appInstallationDirectory()), false); + + installVerifiers.stream().forEach(v -> v.accept(cmd)); + } + + private void verifyPackageUninstalled(JPackageCommand cmd) { + TKit.trace(String.format("Verify uninstalled: %s", + cmd.getPrintableCommandLine())); + if (!cmd.isRuntime()) { + TKit.assertPathExists(cmd.appLauncherPath(), false); + + if (PackageType.WINDOWS.contains(cmd.packageType())) { + new WindowsHelper.AppVerifier(cmd); + } + } + + TKit.assertPathExists(cmd.appInstallationDirectory(), false); + + uninstallVerifiers.stream().forEach(v -> v.accept(cmd)); + } + + private final PackageType type; + private final List> initializers; + private final List> bundleVerifiers; + private final List> installVerifiers; + private final List> uninstallVerifiers; + } + + private Collection currentTypes; + private Set excludeTypes; + private int expectedJPackageExitCode; + private Map handlers; + private Set namedInitializers; + private Action action; + + /** + * Test action. + */ + static public enum Action { + /** + * Create bundle. + */ + CREATE, + /** + * Verify bundle installed. + */ + VERIFY_INSTALL, + /** + * Verify bundle uninstalled. + */ + VERIFY_UNINSTALL; + + @Override + public String toString() { + return name().toLowerCase().replace('_', '-'); + } + }; + private final static Action DEFAULT_ACTION; + private final static File bundleOutputDir; + + static { + final String propertyName = "output"; + String val = TKit.getConfigProperty(propertyName); + if (val == null) { + bundleOutputDir = null; + } else { + bundleOutputDir = new File(val).getAbsoluteFile(); + + if (!bundleOutputDir.isDirectory()) { + throw new IllegalArgumentException(String.format( + "Invalid value of %s sytem property: [%s]. Should be existing directory", + TKit.getConfigPropertyName(propertyName), + bundleOutputDir)); + } + } + } + + static { + final String propertyName = "action"; + String action = Optional.ofNullable(TKit.getConfigProperty(propertyName)).orElse( + Action.CREATE.toString()).toLowerCase(); + DEFAULT_ACTION = Stream.of(Action.values()).filter( + a -> a.toString().equals(action)).findFirst().orElseThrow( + () -> new IllegalArgumentException(String.format( + "Unrecognized value of %s property: [%s]", + TKit.getConfigPropertyName(propertyName), action))); + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageType.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageType.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageType.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * jpackage type traits. + */ +public enum PackageType { + WIN_MSI(".msi", + TKit.isWindows() ? "jdk.incubator.jpackage.internal.WinMsiBundler" : null), + WIN_EXE(".exe", + TKit.isWindows() ? "jdk.incubator.jpackage.internal.WinMsiBundler" : null), + LINUX_DEB(".deb", + TKit.isLinux() ? "jdk.incubator.jpackage.internal.LinuxDebBundler" : null), + LINUX_RPM(".rpm", + TKit.isLinux() ? "jdk.incubator.jpackage.internal.LinuxRpmBundler" : null), + MAC_DMG(".dmg", TKit.isOSX() ? "jdk.incubator.jpackage.internal.MacDmgBundler" : null), + MAC_PKG(".pkg", TKit.isOSX() ? "jdk.incubator.jpackage.internal.MacPkgBundler" : null), + IMAGE("app-image", null, null); + + PackageType(String packageName, String bundleSuffix, String bundlerClass) { + name = packageName; + suffix = bundleSuffix; + if (bundlerClass != null && !Inner.DISABLED_PACKAGERS.contains(getName())) { + supported = isBundlerSupported(bundlerClass); + } else { + supported = false; + } + + if (suffix != null && supported) { + TKit.trace(String.format("Bundler %s supported", getName())); + } + } + + PackageType(String bundleSuffix, String bundlerClass) { + this(bundleSuffix.substring(1), bundleSuffix, bundlerClass); + } + + void applyTo(JPackageCommand cmd) { + cmd.addArguments("--type", getName()); + } + + String getSuffix() { + return suffix; + } + + boolean isSupported() { + return supported; + } + + String getName() { + return name; + } + + static PackageType fromSuffix(String packageFilename) { + if (packageFilename != null) { + for (PackageType v : values()) { + if (packageFilename.endsWith(v.getSuffix())) { + return v; + } + } + } + return null; + } + + private static boolean isBundlerSupported(String bundlerClass) { + try { + Class clazz = Class.forName(bundlerClass); + Method supported = clazz.getMethod("supported", boolean.class); + return ((Boolean) supported.invoke( + clazz.getConstructor().newInstance(), true)); + } catch (ClassNotFoundException | IllegalAccessException ex) { + } catch (InstantiationException | NoSuchMethodException + | InvocationTargetException ex) { + Functional.rethrowUnchecked(ex); + } + return false; + } + + private final String name; + private final String suffix; + private final boolean supported; + + public final static Set LINUX = Set.of(LINUX_DEB, LINUX_RPM); + public final static Set WINDOWS = Set.of(WIN_EXE, WIN_MSI); + public final static Set MAC = Set.of(MAC_PKG, MAC_DMG); + public final static Set NATIVE = Stream.concat( + Stream.concat(LINUX.stream(), WINDOWS.stream()), + MAC.stream()).collect(Collectors.toUnmodifiableSet()); + + private final static class Inner { + + private final static Set DISABLED_PACKAGERS = Optional.ofNullable( + TKit.tokenizeConfigProperty("disabledPackagers")).orElse( + Collections.emptySet()); + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java @@ -0,0 +1,852 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.*; +import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.test.Functional.ExceptionBox; +import jdk.jpackage.test.Functional.ThrowingConsumer; +import jdk.jpackage.test.Functional.ThrowingRunnable; +import jdk.jpackage.test.Functional.ThrowingSupplier; + +final public class TKit { + + private static final String OS = System.getProperty("os.name").toLowerCase(); + + public static final Path TEST_SRC_ROOT = Functional.identity(() -> { + Path root = Path.of(System.getProperty("test.src")); + + for (int i = 0; i != 10; ++i) { + if (root.resolve("apps").toFile().isDirectory()) { + return root.toAbsolutePath(); + } + root = root.resolve(".."); + } + + throw new RuntimeException("Failed to locate apps directory"); + }).get(); + + public final static String ICON_SUFFIX = Functional.identity(() -> { + if (isOSX()) { + return ".icns"; + } + + if (isLinux()) { + return ".png"; + } + + if (isWindows()) { + return ".ico"; + } + + throw throwUnknownPlatformError(); + }).get(); + + public static void run(String args[], ThrowingRunnable testBody) { + if (currentTest != null) { + throw new IllegalStateException( + "Unexpeced nested or concurrent Test.run() call"); + } + + TestInstance test = new TestInstance(testBody); + ThrowingRunnable.toRunnable(() -> runTests(List.of(test))).run(); + test.rethrowIfSkipped(); + if (!test.passed()) { + throw new RuntimeException(); + } + } + + static void withExtraLogStream(ThrowingRunnable action) { + if (extraLogStream != null) { + ThrowingRunnable.toRunnable(action).run(); + } else { + try (PrintStream logStream = openLogStream()) { + extraLogStream = logStream; + ThrowingRunnable.toRunnable(action).run(); + } finally { + extraLogStream = null; + } + } + } + + static void runTests(List tests) { + if (currentTest != null) { + throw new IllegalStateException( + "Unexpeced nested or concurrent Test.run() call"); + } + + withExtraLogStream(() -> { + tests.stream().forEach(test -> { + currentTest = test; + try { + ignoreExceptions(test).run(); + } finally { + currentTest = null; + if (extraLogStream != null) { + extraLogStream.flush(); + } + } + }); + }); + } + + static Runnable ignoreExceptions(ThrowingRunnable action) { + return () -> { + try { + try { + action.run(); + } catch (Throwable ex) { + unbox(ex); + } + } catch (Throwable throwable) { + printStackTrace(throwable); + } + }; + } + + static void unbox(Throwable throwable) throws Throwable { + try { + throw throwable; + } catch (ExceptionBox | InvocationTargetException ex) { + unbox(ex.getCause()); + } + } + + public static Path workDir() { + return currentTest.workDir(); + } + + static Path defaultInputDir() { + return workDir().resolve("input"); + } + + static Path defaultOutputDir() { + return workDir().resolve("output"); + } + + static String getCurrentDefaultAppName() { + // Construct app name from swapping and joining test base name + // and test function name. + // Say the test name is `FooTest.testBasic`. Then app name would be `BasicFooTest`. + String appNamePrefix = currentTest.functionName(); + if (appNamePrefix != null && appNamePrefix.startsWith("test")) { + appNamePrefix = appNamePrefix.substring("test".length()); + } + return Stream.of(appNamePrefix, currentTest.baseName()).filter( + v -> v != null && !v.isEmpty()).collect(Collectors.joining()); + } + + public static boolean isWindows() { + return (OS.contains("win")); + } + + public static boolean isOSX() { + return (OS.contains("mac")); + } + + public static boolean isLinux() { + return ((OS.contains("nix") || OS.contains("nux"))); + } + + static void log(String v) { + System.out.println(v); + if (extraLogStream != null) { + extraLogStream.println(v); + } + } + + public static void createTextFile(Path propsFilename, Collection lines) { + createTextFile(propsFilename, lines.stream()); + } + + public static void createTextFile(Path propsFilename, Stream lines) { + trace(String.format("Create [%s] text file...", + propsFilename.toAbsolutePath().normalize())); + ThrowingRunnable.toRunnable(() -> Files.write(propsFilename, + lines.peek(TKit::trace).collect(Collectors.toList()))).run(); + trace("Done"); + } + + public static void createPropertiesFile(Path propsFilename, + Collection> props) { + trace(String.format("Create [%s] properties file...", + propsFilename.toAbsolutePath().normalize())); + ThrowingRunnable.toRunnable(() -> Files.write(propsFilename, + props.stream().map(e -> String.join("=", e.getKey(), + e.getValue())).peek(TKit::trace).collect(Collectors.toList()))).run(); + trace("Done"); + } + + public static void createPropertiesFile(Path propsFilename, + Map.Entry... props) { + createPropertiesFile(propsFilename, List.of(props)); + } + + public static void createPropertiesFile(Path propsFilename, + Map props) { + createPropertiesFile(propsFilename, props.entrySet()); + } + + public static void trace(String v) { + if (TRACE) { + log("TRACE: " + v); + } + } + + private static void traceAssert(String v) { + if (TRACE_ASSERTS) { + log("TRACE: " + v); + } + } + + public static void error(String v) { + log("ERROR: " + v); + throw new AssertionError(v); + } + + private final static String TEMP_FILE_PREFIX = null; + + private static Path createUniqueFileName(String defaultName) { + final String[] nameComponents; + + int separatorIdx = defaultName.lastIndexOf('.'); + final String baseName; + if (separatorIdx == -1) { + baseName = defaultName; + nameComponents = new String[]{baseName}; + } else { + baseName = defaultName.substring(0, separatorIdx); + nameComponents = new String[]{baseName, defaultName.substring( + separatorIdx + 1)}; + } + + final Path basedir = workDir(); + int i = 0; + for (; i < 100; ++i) { + Path path = basedir.resolve(String.join(".", nameComponents)); + if (!path.toFile().exists()) { + return path; + } + nameComponents[0] = String.format("%s.%d", baseName, i); + } + throw new IllegalStateException(String.format( + "Failed to create unique file name from [%s] basename after %d attempts", + baseName, i)); + } + + public static Path createTempDirectory(String role) throws IOException { + if (role == null) { + return Files.createTempDirectory(workDir(), TEMP_FILE_PREFIX); + } + return Files.createDirectory(createUniqueFileName(role)); + } + + public static Path createTempFile(String role, String suffix) throws + IOException { + if (role == null) { + return Files.createTempFile(workDir(), TEMP_FILE_PREFIX, suffix); + } + return Files.createFile(createUniqueFileName(role)); + } + + public static Path withTempFile(String role, String suffix, + ThrowingConsumer action) { + final Path tempFile = ThrowingSupplier.toSupplier(() -> createTempFile( + role, suffix)).get(); + boolean keepIt = true; + try { + ThrowingConsumer.toConsumer(action).accept(tempFile); + keepIt = false; + return tempFile; + } finally { + if (tempFile != null && !keepIt) { + ThrowingRunnable.toRunnable(() -> Files.deleteIfExists(tempFile)).run(); + } + } + } + + public static Path withTempDirectory(String role, + ThrowingConsumer action) { + final Path tempDir = ThrowingSupplier.toSupplier( + () -> createTempDirectory(role)).get(); + boolean keepIt = true; + try { + ThrowingConsumer.toConsumer(action).accept(tempDir); + keepIt = false; + return tempDir; + } finally { + if (tempDir != null && tempDir.toFile().isDirectory() && !keepIt) { + deleteDirectoryRecursive(tempDir, ""); + } + } + } + + private static class DirectoryCleaner implements Consumer { + DirectoryCleaner traceMessage(String v) { + msg = v; + return this; + } + + DirectoryCleaner contentsOnly(boolean v) { + contentsOnly = v; + return this; + } + + @Override + public void accept(Path root) { + if (msg == null) { + if (contentsOnly) { + msg = String.format("Cleaning [%s] directory recursively", + root); + } else { + msg = String.format("Deleting [%s] directory recursively", + root); + } + } + + if (!msg.isEmpty()) { + trace(msg); + } + + List errors = new ArrayList<>(); + try { + final List paths; + if (contentsOnly) { + try (var pathStream = Files.walk(root, 0)) { + paths = pathStream.collect(Collectors.toList()); + } + } else { + paths = List.of(root); + } + + for (var path : paths) { + try (var pathStream = Files.walk(path)) { + pathStream + .sorted(Comparator.reverseOrder()) + .sequential() + .forEachOrdered(file -> { + try { + if (isWindows()) { + Files.setAttribute(file, "dos:readonly", false); + } + Files.delete(file); + } catch (IOException ex) { + errors.add(ex); + } + }); + } + } + + } catch (IOException ex) { + errors.add(ex); + } + errors.forEach(error -> trace(error.toString())); + } + + private String msg; + private boolean contentsOnly; + } + + /** + * Deletes contents of the given directory recursively. Shortcut for + * deleteDirectoryContentsRecursive(path, null) + * + * @param path path to directory to clean + */ + public static void deleteDirectoryContentsRecursive(Path path) { + deleteDirectoryContentsRecursive(path, null); + } + + /** + * Deletes contents of the given directory recursively. If path is not a + * directory, request is silently ignored. + * + * @param path path to directory to clean + * @param msg log message. If null, the default log message is used. If + * empty string, no log message will be saved. + */ + public static void deleteDirectoryContentsRecursive(Path path, String msg) { + if (path.toFile().isDirectory()) { + new DirectoryCleaner().contentsOnly(true).traceMessage(msg).accept( + path); + } + } + + /** + * Deletes the given directory recursively. Shortcut for + * deleteDirectoryRecursive(path, null) + * + * @param path path to directory to delete + */ + public static void deleteDirectoryRecursive(Path path) { + deleteDirectoryRecursive(path, null); + } + + /** + * Deletes the given directory recursively. If path is not a + * directory, request is silently ignored. + * + * @param path path to directory to delete + * @param msg log message. If null, the default log message is used. If + * empty string, no log message will be saved. + */ + public static void deleteDirectoryRecursive(Path path, String msg) { + if (path.toFile().isDirectory()) { + new DirectoryCleaner().traceMessage(msg).accept(path); + } + } + + public static RuntimeException throwUnknownPlatformError() { + if (isWindows() || isLinux() || isOSX()) { + throw new IllegalStateException( + "Platform is known. throwUnknownPlatformError() called by mistake"); + } + throw new IllegalStateException("Unknown platform"); + } + + public static RuntimeException throwSkippedException(String reason) { + trace("Skip the test: " + reason); + RuntimeException ex = ThrowingSupplier.toSupplier( + () -> (RuntimeException) Class.forName("jtreg.SkippedException").getConstructor( + String.class).newInstance(reason)).get(); + + currentTest.notifySkipped(ex); + throw ex; + } + + public static Path createRelativePathCopy(final Path file) { + Path fileCopy = workDir().resolve(file.getFileName()).toAbsolutePath().normalize(); + + ThrowingRunnable.toRunnable(() -> Files.copy(file, fileCopy, + StandardCopyOption.REPLACE_EXISTING)).run(); + + final Path basePath = Path.of(".").toAbsolutePath().normalize(); + try { + return basePath.relativize(fileCopy); + } catch (IllegalArgumentException ex) { + // May happen on Windows: java.lang.IllegalArgumentException: 'other' has different root + trace(String.format("Failed to relativize [%s] at [%s]", fileCopy, + basePath)); + printStackTrace(ex); + } + return file; + } + + static void waitForFileCreated(Path fileToWaitFor, + long timeoutSeconds) throws IOException { + + trace(String.format("Wait for file [%s] to be available", + fileToWaitFor.toAbsolutePath())); + + WatchService ws = FileSystems.getDefault().newWatchService(); + + Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent(); + watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY); + + long waitUntil = System.currentTimeMillis() + timeoutSeconds * 1000; + for (;;) { + long timeout = waitUntil - System.currentTimeMillis(); + assertTrue(timeout > 0, String.format( + "Check timeout value %d is positive", timeout)); + + WatchKey key = ThrowingSupplier.toSupplier(() -> ws.poll(timeout, + TimeUnit.MILLISECONDS)).get(); + if (key == null) { + if (fileToWaitFor.toFile().exists()) { + trace(String.format( + "File [%s] is available after poll timeout expired", + fileToWaitFor)); + return; + } + assertUnexpected(String.format("Timeout expired", timeout)); + } + + for (WatchEvent event : key.pollEvents()) { + if (event.kind() == StandardWatchEventKinds.OVERFLOW) { + continue; + } + Path contextPath = (Path) event.context(); + if (Files.isSameFile(watchDirectory.resolve(contextPath), + fileToWaitFor)) { + trace(String.format("File [%s] is available", fileToWaitFor)); + return; + } + } + + if (!key.reset()) { + assertUnexpected("Watch key invalidated"); + } + } + } + + static void printStackTrace(Throwable throwable) { + if (extraLogStream != null) { + throwable.printStackTrace(extraLogStream); + } + throwable.printStackTrace(); + } + + private static String concatMessages(String msg, String msg2) { + if (msg2 != null && !msg2.isBlank()) { + return msg + ": " + msg2; + } + return msg; + } + + public static void assertEquals(long expected, long actual, String msg) { + currentTest.notifyAssert(); + if (expected != actual) { + error(concatMessages(String.format( + "Expected [%d]. Actual [%d]", expected, actual), + msg)); + } + + traceAssert(String.format("assertEquals(%d): %s", expected, msg)); + } + + public static void assertNotEquals(long expected, long actual, String msg) { + currentTest.notifyAssert(); + if (expected == actual) { + error(concatMessages(String.format("Unexpected [%d] value", actual), + msg)); + } + + traceAssert(String.format("assertNotEquals(%d, %d): %s", expected, + actual, msg)); + } + + public static void assertEquals(String expected, String actual, String msg) { + currentTest.notifyAssert(); + if ((actual != null && !actual.equals(expected)) + || (expected != null && !expected.equals(actual))) { + error(concatMessages(String.format( + "Expected [%s]. Actual [%s]", expected, actual), + msg)); + } + + traceAssert(String.format("assertEquals(%s): %s", expected, msg)); + } + + public static void assertNotEquals(String expected, String actual, String msg) { + currentTest.notifyAssert(); + if ((actual != null && !actual.equals(expected)) + || (expected != null && !expected.equals(actual))) { + + traceAssert(String.format("assertNotEquals(%s, %s): %s", expected, + actual, msg)); + return; + } + + error(concatMessages(String.format("Unexpected [%s] value", actual), msg)); + } + + public static void assertNull(Object value, String msg) { + currentTest.notifyAssert(); + if (value != null) { + error(concatMessages(String.format("Unexpected not null value [%s]", + value), msg)); + } + + traceAssert(String.format("assertNull(): %s", msg)); + } + + public static void assertNotNull(Object value, String msg) { + currentTest.notifyAssert(); + if (value == null) { + error(concatMessages("Unexpected null value", msg)); + } + + traceAssert(String.format("assertNotNull(%s): %s", value, msg)); + } + + public static void assertTrue(boolean actual, String msg) { + assertTrue(actual, msg, null); + } + + public static void assertFalse(boolean actual, String msg) { + assertFalse(actual, msg, null); + } + + public static void assertTrue(boolean actual, String msg, Runnable onFail) { + currentTest.notifyAssert(); + if (!actual) { + if (onFail != null) { + onFail.run(); + } + error(concatMessages("Failed", msg)); + } + + traceAssert(String.format("assertTrue(): %s", msg)); + } + + public static void assertFalse(boolean actual, String msg, Runnable onFail) { + currentTest.notifyAssert(); + if (actual) { + if (onFail != null) { + onFail.run(); + } + error(concatMessages("Failed", msg)); + } + + traceAssert(String.format("assertFalse(): %s", msg)); + } + + public static void assertPathExists(Path path, boolean exists) { + if (exists) { + assertTrue(path.toFile().exists(), String.format( + "Check [%s] path exists", path)); + } else { + assertFalse(path.toFile().exists(), String.format( + "Check [%s] path doesn't exist", path)); + } + } + + public static void assertDirectoryExists(Path path) { + assertPathExists(path, true); + assertTrue(path.toFile().isDirectory(), String.format( + "Check [%s] is a directory", path)); + } + + public static void assertFileExists(Path path) { + assertPathExists(path, true); + assertTrue(path.toFile().isFile(), String.format("Check [%s] is a file", + path)); + } + + public static void assertExecutableFileExists(Path path) { + assertFileExists(path); + assertTrue(path.toFile().canExecute(), String.format( + "Check [%s] file is executable", path)); + } + + public static void assertReadableFileExists(Path path) { + assertFileExists(path); + assertTrue(path.toFile().canRead(), String.format( + "Check [%s] file is readable", path)); + } + + public static void assertUnexpected(String msg) { + currentTest.notifyAssert(); + error(concatMessages("Unexpected", msg)); + } + + public static void assertStringListEquals(List expected, + List actual, String msg) { + currentTest.notifyAssert(); + + traceAssert(String.format("assertStringListEquals(): %s", msg)); + + String idxFieldFormat = Functional.identity(() -> { + int listSize = expected.size(); + int width = 0; + while (listSize != 0) { + listSize = listSize / 10; + width++; + } + return "%" + width + "d"; + }).get(); + + AtomicInteger counter = new AtomicInteger(0); + Iterator actualIt = actual.iterator(); + expected.stream().sequential().filter(expectedStr -> actualIt.hasNext()).forEach(expectedStr -> { + int idx = counter.incrementAndGet(); + String actualStr = actualIt.next(); + + if ((actualStr != null && !actualStr.equals(expectedStr)) + || (expectedStr != null && !expectedStr.equals(actualStr))) { + error(concatMessages(String.format( + "(" + idxFieldFormat + ") Expected [%s]. Actual [%s]", + idx, expectedStr, actualStr), msg)); + } + + traceAssert(String.format( + "assertStringListEquals(" + idxFieldFormat + ", %s)", idx, + expectedStr)); + }); + + if (expected.size() < actual.size()) { + // Actual string list is longer than expected + error(concatMessages(String.format( + "Actual list is longer than expected by %d elements", + actual.size() - expected.size()), msg)); + } + + if (actual.size() < expected.size()) { + // Actual string list is shorter than expected + error(concatMessages(String.format( + "Actual list is longer than expected by %d elements", + expected.size() - actual.size()), msg)); + } + } + + public final static class TextStreamAsserter { + TextStreamAsserter(String value) { + this.value = value; + predicate(String::contains); + } + + public TextStreamAsserter label(String v) { + label = v; + return this; + } + + public TextStreamAsserter predicate(BiPredicate v) { + predicate = v; + return this; + } + + public TextStreamAsserter negate() { + negate = true; + return this; + } + + public TextStreamAsserter orElseThrow(RuntimeException v) { + return orElseThrow(() -> v); + } + + public TextStreamAsserter orElseThrow(Supplier v) { + createException = v; + return this; + } + + public void apply(Stream lines) { + String matchedStr = lines.filter(line -> predicate.test(line, value)).findFirst().orElse( + null); + String labelStr = Optional.ofNullable(label).orElse("output"); + if (negate) { + String msg = String.format( + "Check %s doesn't contain [%s] string", labelStr, value); + if (createException == null) { + assertNull(matchedStr, msg); + } else { + trace(msg); + if (matchedStr != null) { + throw createException.get(); + } + } + } else { + String msg = String.format("Check %s contains [%s] string", + labelStr, value); + if (createException == null) { + assertNotNull(matchedStr, msg); + } else { + trace(msg); + if (matchedStr == null) { + throw createException.get(); + } + } + } + } + + private BiPredicate predicate; + private String label; + private boolean negate; + private Supplier createException; + final private String value; + } + + public static TextStreamAsserter assertTextStream(String what) { + return new TextStreamAsserter(what); + } + + private static PrintStream openLogStream() { + if (LOG_FILE == null) { + return null; + } + + return ThrowingSupplier.toSupplier(() -> new PrintStream( + new FileOutputStream(LOG_FILE.toFile(), true))).get(); + } + + private static TestInstance currentTest; + private static PrintStream extraLogStream; + + private static final boolean TRACE; + private static final boolean TRACE_ASSERTS; + + static final boolean VERBOSE_JPACKAGE; + static final boolean VERBOSE_TEST_SETUP; + + static String getConfigProperty(String propertyName) { + return System.getProperty(getConfigPropertyName(propertyName)); + } + + static String getConfigPropertyName(String propertyName) { + return "jpackage.test." + propertyName; + } + + static Set tokenizeConfigProperty(String propertyName) { + final String val = TKit.getConfigProperty(propertyName); + if (val == null) { + return null; + } + return Stream.of(val.toLowerCase().split(",")).map(String::strip).filter( + Predicate.not(String::isEmpty)).collect(Collectors.toSet()); + } + + static final Path LOG_FILE = Functional.identity(() -> { + String val = getConfigProperty("logfile"); + if (val == null) { + return null; + } + return Path.of(val); + }).get(); + + static { + Set logOptions = tokenizeConfigProperty("suppress-logging"); + if (logOptions == null) { + TRACE = true; + TRACE_ASSERTS = true; + VERBOSE_JPACKAGE = true; + VERBOSE_TEST_SETUP = true; + } else if (logOptions.contains("all")) { + TRACE = false; + TRACE_ASSERTS = false; + VERBOSE_JPACKAGE = false; + VERBOSE_TEST_SETUP = false; + } else { + Predicate> isNonOf = options -> { + return Collections.disjoint(logOptions, options); + }; + + TRACE = isNonOf.test(Set.of("trace", "t")); + TRACE_ASSERTS = isNonOf.test(Set.of("assert", "a")); + VERBOSE_JPACKAGE = isNonOf.test(Set.of("jpackage", "jp")); + VERBOSE_TEST_SETUP = isNonOf.test(Set.of("init", "i")); + } + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java @@ -0,0 +1,462 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.test; + +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.test.Annotations.AfterEach; +import jdk.jpackage.test.Annotations.BeforeEach; +import jdk.jpackage.test.Annotations.Parameter; +import jdk.jpackage.test.Annotations.ParameterGroup; +import jdk.jpackage.test.Annotations.Parameters; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.Functional.ThrowingConsumer; +import jdk.jpackage.test.Functional.ThrowingFunction; + +final class TestBuilder implements AutoCloseable { + + @Override + public void close() throws Exception { + flushTestGroup(); + } + + TestBuilder(Consumer testConsumer) { + argProcessors = Map.of( + CMDLINE_ARG_PREFIX + "after-run", + arg -> getJavaMethodsFromArg(arg).map( + this::wrap).forEachOrdered(afterActions::add), + + CMDLINE_ARG_PREFIX + "before-run", + arg -> getJavaMethodsFromArg(arg).map( + this::wrap).forEachOrdered(beforeActions::add), + + CMDLINE_ARG_PREFIX + "run", + arg -> addTestGroup(getJavaMethodsFromArg(arg).map( + ThrowingFunction.toFunction( + TestBuilder::toMethodCalls)).flatMap(s -> s).collect( + Collectors.toList())), + + CMDLINE_ARG_PREFIX + "exclude", + arg -> (excludedTests = Optional.ofNullable( + excludedTests).orElse(new HashSet())).add(arg), + + CMDLINE_ARG_PREFIX + "include", + arg -> (includedTests = Optional.ofNullable( + includedTests).orElse(new HashSet())).add(arg), + + CMDLINE_ARG_PREFIX + "space-subst", + arg -> spaceSubstitute = arg, + + CMDLINE_ARG_PREFIX + "group", + arg -> flushTestGroup(), + + CMDLINE_ARG_PREFIX + "dry-run", + arg -> dryRun = true + ); + this.testConsumer = testConsumer; + clear(); + } + + void processCmdLineArg(String arg) throws Throwable { + int separatorIdx = arg.indexOf('='); + final String argName; + final String argValue; + if (separatorIdx != -1) { + argName = arg.substring(0, separatorIdx); + argValue = arg.substring(separatorIdx + 1); + } else { + argName = arg; + argValue = null; + } + try { + ThrowingConsumer argProcessor = argProcessors.get(argName); + if (argProcessor == null) { + throw new ParseException("Unrecognized"); + } + argProcessor.accept(argValue); + } catch (ParseException ex) { + ex.setContext(arg); + throw ex; + } + } + + private void addTestGroup(List newTestGroup) { + if (testGroup != null) { + testGroup.addAll(newTestGroup); + } else { + testGroup = newTestGroup; + } + } + + private static Stream filterTests(Stream tests, + Set filters, UnaryOperator pred, String logMsg) { + if (filters == null) { + return tests; + } + + // Log all matches before returning from the function + return tests.filter(test -> { + String testDescription = test.createDescription().testFullName(); + boolean match = filters.stream().anyMatch( + v -> testDescription.contains(v)); + if (match) { + trace(String.format(logMsg + ": %s", testDescription)); + } + return pred.apply(match); + }).collect(Collectors.toList()).stream(); + } + + private Stream filterTestGroup() { + Objects.requireNonNull(testGroup); + + UnaryOperator> restoreSpaces = filters -> { + if (spaceSubstitute == null || filters == null) { + return filters; + } + return filters.stream().map( + filter -> filter.replace(spaceSubstitute, " ")).collect( + Collectors.toSet()); + }; + + if (includedTests != null) { + return filterTests(testGroup.stream(), restoreSpaces.apply( + includedTests), x -> x, "Include"); + } + + return filterTests(testGroup.stream(), + restoreSpaces.apply(excludedTests), x -> !x, "Exclude"); + } + + private void flushTestGroup() { + if (testGroup != null) { + filterTestGroup().forEach(testBody -> createTestInstance(testBody)); + clear(); + } + } + + private void createTestInstance(MethodCall testBody) { + final List curBeforeActions; + final List curAfterActions; + + Method testMethod = testBody.getMethod(); + if (Stream.of(BeforeEach.class, AfterEach.class).anyMatch( + type -> testMethod.isAnnotationPresent(type))) { + curBeforeActions = beforeActions; + curAfterActions = afterActions; + } else { + curBeforeActions = new ArrayList<>(beforeActions); + curAfterActions = new ArrayList<>(afterActions); + + selectFrameMethods(testMethod.getDeclaringClass(), BeforeEach.class).map( + this::wrap).forEachOrdered(curBeforeActions::add); + selectFrameMethods(testMethod.getDeclaringClass(), AfterEach.class).map( + this::wrap).forEachOrdered(curAfterActions::add); + } + + TestInstance test = new TestInstance(testBody, curBeforeActions, + curAfterActions, dryRun); + if (includedTests == null) { + trace(String.format("Create: %s", test.fullName())); + } + testConsumer.accept(test); + } + + private void clear() { + beforeActions = new ArrayList<>(); + afterActions = new ArrayList<>(); + excludedTests = null; + includedTests = null; + spaceSubstitute = null; + testGroup = null; + } + + private static Class probeClass(String name) { + try { + return Class.forName(name); + } catch (ClassNotFoundException ex) { + return null; + } + } + + private static Stream selectFrameMethods(Class type, Class annotationType) { + return Stream.of(type.getMethods()) + .filter(m -> m.getParameterCount() == 0) + .filter(m -> !m.isAnnotationPresent(Test.class)) + .filter(m -> m.isAnnotationPresent(annotationType)) + .sorted((a, b) -> a.getName().compareTo(b.getName())); + } + + private static Stream cmdLineArgValueToMethodNames(String v) { + List result = new ArrayList<>(); + String defaultClassName = null; + for (String token : v.split(",")) { + Class testSet = probeClass(token); + if (testSet != null) { + // Test set class specified. Pull in all public methods + // from the class with @Test annotation removing name duplicates. + // Overloads will be handled at the next phase of processing. + defaultClassName = token; + Stream.of(testSet.getMethods()).filter( + m -> m.isAnnotationPresent(Test.class)).map( + Method::getName).distinct().forEach( + name -> result.add(String.join(".", token, name))); + + continue; + } + + final String qualifiedMethodName; + final int lastDotIdx = token.lastIndexOf('.'); + if (lastDotIdx != -1) { + qualifiedMethodName = token; + defaultClassName = token.substring(0, lastDotIdx); + } else if (defaultClassName == null) { + throw new ParseException("Default class name not found in"); + } else { + qualifiedMethodName = String.join(".", defaultClassName, token); + } + result.add(qualifiedMethodName); + } + return result.stream(); + } + + private static boolean filterMethod(String expectedMethodName, Method method) { + if (!method.getName().equals(expectedMethodName)) { + return false; + } + switch (method.getParameterCount()) { + case 0: + return !isParametrized(method); + case 1: + return isParametrized(method); + } + return false; + } + + private static boolean isParametrized(Method method) { + return method.isAnnotationPresent(ParameterGroup.class) || method.isAnnotationPresent( + Parameter.class); + } + + private static List getJavaMethodFromString( + String qualifiedMethodName) { + int lastDotIdx = qualifiedMethodName.lastIndexOf('.'); + if (lastDotIdx == -1) { + throw new ParseException("Class name not found in"); + } + String className = qualifiedMethodName.substring(0, lastDotIdx); + String methodName = qualifiedMethodName.substring(lastDotIdx + 1); + Class methodClass; + try { + methodClass = Class.forName(className); + } catch (ClassNotFoundException ex) { + throw new ParseException(String.format("Class [%s] not found;", + className)); + } + // Get the list of all public methods as need to deal with overloads. + List methods = Stream.of(methodClass.getMethods()).filter( + (m) -> filterMethod(methodName, m)).collect(Collectors.toList()); + if (methods.isEmpty()) { + new ParseException(String.format( + "Method [%s] not found in [%s] class;", + methodName, className)); + } + + trace(String.format("%s -> %s", qualifiedMethodName, methods)); + return methods; + } + + private static Stream getJavaMethodsFromArg(String argValue) { + return cmdLineArgValueToMethodNames(argValue).map( + ThrowingFunction.toFunction( + TestBuilder::getJavaMethodFromString)).flatMap( + List::stream).sequential(); + } + + private static Parameter[] getMethodParameters(Method method) { + if (method.isAnnotationPresent(ParameterGroup.class)) { + return ((ParameterGroup) method.getAnnotation(ParameterGroup.class)).value(); + } + + if (method.isAnnotationPresent(Parameter.class)) { + return new Parameter[]{(Parameter) method.getAnnotation( + Parameter.class)}; + } + + // Unexpected + return null; + } + + private static Stream toCtorArgs(Method method) throws + IllegalAccessException, InvocationTargetException { + Class type = method.getDeclaringClass(); + List paremetersProviders = Stream.of(type.getMethods()) + .filter(m -> m.getParameterCount() == 0) + .filter(m -> (m.getModifiers() & Modifier.STATIC) != 0) + .filter(m -> m.isAnnotationPresent(Parameters.class)) + .sorted() + .collect(Collectors.toList()); + if (paremetersProviders.isEmpty()) { + // Single instance using the default constructor. + return Stream.ofNullable(MethodCall.DEFAULT_CTOR_ARGS); + } + + // Pick the first method from the list. + Method paremetersProvider = paremetersProviders.iterator().next(); + if (paremetersProviders.size() > 1) { + trace(String.format( + "Found %d public static methods without arguments with %s annotation. Will use %s", + paremetersProviders.size(), Parameters.class, + paremetersProvider)); + paremetersProviders.stream().map(Method::toString).forEach( + TestBuilder::trace); + } + + // Construct collection of arguments for test class instances. + return ((Collection) paremetersProvider.invoke(null)).stream(); + } + + private static Stream toMethodCalls(Method method) throws + IllegalAccessException, InvocationTargetException { + return toCtorArgs(method).map(v -> toMethodCalls(v, method)).flatMap( + s -> s).peek(methodCall -> { + // Make sure required constructor is accessible if the one is needed. + // Need to probe all methods as some of them might be static + // and some class members. + // Only class members require ctors. + try { + methodCall.checkRequiredConstructor(); + } catch (NoSuchMethodException ex) { + throw new ParseException(ex.getMessage() + "."); + } + }); + } + + private static Stream toMethodCalls(Object[] ctorArgs, Method method) { + if (!isParametrized(method)) { + return Stream.of(new MethodCall(ctorArgs, method)); + } + Parameter[] annotations = getMethodParameters(method); + if (annotations.length == 0) { + return Stream.of(new MethodCall(ctorArgs, method)); + } + return Stream.of(annotations).map((a) -> { + Class paramType = method.getParameterTypes()[0]; + final Object annotationValue; + if (!paramType.isArray()) { + annotationValue = fromString(a.value()[0], paramType); + } else { + Class paramComponentType = paramType.getComponentType(); + annotationValue = Array.newInstance(paramComponentType, a.value().length); + var idx = new AtomicInteger(-1); + Stream.of(a.value()).map(v -> fromString(v, paramComponentType)).sequential().forEach( + v -> Array.set(annotationValue, idx.incrementAndGet(), v)); + } + return new MethodCall(ctorArgs, method, annotationValue); + }); + } + + private static Object fromString(String value, Class toType) { + Function converter = conv.get(toType); + if (converter == null) { + throw new RuntimeException(String.format( + "Failed to find a conversion of [%s] string to %s type", + value, toType)); + } + return converter.apply(value); + } + + // Wraps Method.invike() into ThrowingRunnable.run() + private ThrowingConsumer wrap(Method method) { + return (test) -> { + Class methodClass = method.getDeclaringClass(); + String methodName = String.join(".", methodClass.getName(), + method.getName()); + TKit.log(String.format("[ CALL ] %s()", methodName)); + if (!dryRun) { + if (methodClass.isInstance(test)) { + method.invoke(test); + } else { + method.invoke(null); + } + } + }; + } + + private static class ParseException extends IllegalArgumentException { + + ParseException(String msg) { + super(msg); + } + + void setContext(String badCmdLineArg) { + this.badCmdLineArg = badCmdLineArg; + } + + @Override + public String getMessage() { + String msg = super.getMessage(); + if (badCmdLineArg != null) { + msg = String.format("%s parameter=[%s]", msg, badCmdLineArg); + } + return msg; + } + private String badCmdLineArg; + } + + static void trace(String msg) { + if (TKit.VERBOSE_TEST_SETUP) { + TKit.log(msg); + } + } + + private final Map> argProcessors; + private Consumer testConsumer; + private List testGroup; + private List beforeActions; + private List afterActions; + private Set excludedTests; + private Set includedTests; + private String spaceSubstitute; + private boolean dryRun; + + private final static Map> conv = Map.of( + boolean.class, Boolean::valueOf, + Boolean.class, Boolean::valueOf, + int.class, Integer::valueOf, + Integer.class, Integer::valueOf, + long.class, Long::valueOf, + Long.class, Long::valueOf, + String.class, String::valueOf); + + final static String CMDLINE_ARG_PREFIX = "--jpt-"; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.test; + +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.test.Functional.ThrowingConsumer; +import jdk.jpackage.test.Functional.ThrowingFunction; +import jdk.jpackage.test.Functional.ThrowingRunnable; + +final class TestInstance implements ThrowingRunnable { + + static class TestDesc { + private TestDesc() { + } + + String testFullName() { + StringBuilder sb = new StringBuilder(); + sb.append(clazz.getSimpleName()); + if (instanceArgs != null) { + sb.append('(').append(instanceArgs).append(')'); + } + if (functionName != null) { + sb.append('.'); + sb.append(functionName); + if (functionArgs != null) { + sb.append('(').append(functionArgs).append(')'); + } + } + return sb.toString(); + } + + static Builder createBuilder() { + return new Builder(); + } + + static final class Builder implements Supplier { + private Builder() { + } + + Builder method(Method v) { + method = v; + return this; + } + + Builder ctorArgs(Object... v) { + ctorArgs = ofNullable(v); + return this; + } + + Builder methodArgs(Object... v) { + methodArgs = ofNullable(v); + return this; + } + + @Override + public TestDesc get() { + TestDesc desc = new TestDesc(); + if (method == null) { + desc.clazz = enclosingMainMethodClass(); + } else { + desc.clazz = method.getDeclaringClass(); + desc.functionName = method.getName(); + desc.functionArgs = formatArgs(methodArgs); + desc.instanceArgs = formatArgs(ctorArgs); + } + return desc; + } + + private static String formatArgs(List values) { + if (values == null) { + return null; + } + return values.stream().map(v -> { + if (v != null && v.getClass().isArray()) { + return String.format("%s(length=%d)", + Arrays.deepToString((Object[]) v), + Array.getLength(v)); + } + return String.format("%s", v); + }).collect(Collectors.joining(", ")); + } + + private static List ofNullable(Object... values) { + List result = new ArrayList(); + for (var v: values) { + result.add(v); + } + return result; + } + + private List ctorArgs; + private List methodArgs; + private Method method; + } + + static TestDesc create(Method m, Object... args) { + TestDesc desc = new TestDesc(); + desc.clazz = m.getDeclaringClass(); + desc.functionName = m.getName(); + if (args.length != 0) { + desc.functionArgs = Stream.of(args).map(v -> { + if (v.getClass().isArray()) { + return String.format("%s(length=%d)", + Arrays.deepToString((Object[]) v), + Array.getLength(v)); + } + return String.format("%s", v); + }).collect(Collectors.joining(", ")); + } + return desc; + } + + private Class clazz; + private String functionName; + private String functionArgs; + private String instanceArgs; + } + + TestInstance(ThrowingRunnable testBody) { + assertCount = 0; + this.testConstructor = (unused) -> null; + this.testBody = (unused) -> testBody.run(); + this.beforeActions = Collections.emptyList(); + this.afterActions = Collections.emptyList(); + this.testDesc = TestDesc.createBuilder().get(); + this.dryRun = false; + this.workDir = createWorkDirName(testDesc); + } + + TestInstance(MethodCall testBody, List beforeActions, + List afterActions, boolean dryRun) { + assertCount = 0; + this.testConstructor = v -> ((MethodCall)v).newInstance(); + this.testBody = testBody; + this.beforeActions = beforeActions; + this.afterActions = afterActions; + this.testDesc = testBody.createDescription(); + this.dryRun = dryRun; + this.workDir = createWorkDirName(testDesc); + } + + void notifyAssert() { + assertCount++; + } + + void notifySkipped(RuntimeException ex) { + skippedTestException = ex; + } + + boolean passed() { + return status == Status.Passed; + } + + boolean skipped() { + return status == Status.Skipped; + } + + boolean failed() { + return status == Status.Failed; + } + + String functionName() { + return testDesc.functionName; + } + + String baseName() { + return testDesc.clazz.getSimpleName(); + } + + String fullName() { + return testDesc.testFullName(); + } + + void rethrowIfSkipped() { + if (skippedTestException != null) { + throw skippedTestException; + } + } + + Path workDir() { + return workDir; + } + + @Override + public void run() throws Throwable { + final String fullName = fullName(); + TKit.log(String.format("[ RUN ] %s", fullName)); + try { + Object testInstance = testConstructor.apply(testBody); + beforeActions.forEach(a -> ThrowingConsumer.toConsumer(a).accept( + testInstance)); + try { + if (!dryRun) { + Files.createDirectories(workDir); + testBody.accept(testInstance); + } + } finally { + afterActions.forEach(a -> TKit.ignoreExceptions(() -> a.accept( + testInstance))); + } + status = Status.Passed; + } finally { + if (skippedTestException != null) { + status = Status.Skipped; + } else if (status == null) { + status = Status.Failed; + } + + if (!KEEP_WORK_DIR.contains(status)) { + TKit.deleteDirectoryRecursive(workDir); + } + + TKit.log(String.format("%s %s; checks=%d", status, fullName, + assertCount)); + } + } + + private static Class enclosingMainMethodClass() { + StackTraceElement st[] = Thread.currentThread().getStackTrace(); + for (StackTraceElement ste : st) { + if ("main".equals(ste.getMethodName())) { + return Functional.ThrowingSupplier.toSupplier(() -> Class.forName( + ste.getClassName())).get(); + } + } + return null; + } + + private static boolean isCalledByJavatest() { + StackTraceElement st[] = Thread.currentThread().getStackTrace(); + for (StackTraceElement ste : st) { + if (ste.getClassName().startsWith("com.sun.javatest.")) { + return true; + } + } + return false; + } + + private static Path createWorkDirName(TestDesc testDesc) { + Path result = Path.of("."); + if (!isCalledByJavatest()) { + result = result.resolve(testDesc.clazz.getSimpleName()); + } + + List components = new ArrayList<>(); + + final String testFunctionName = testDesc.functionName; + if (testFunctionName != null) { + components.add(testFunctionName); + } + + final boolean isPrametrized = Stream.of(testDesc.functionArgs, + testDesc.instanceArgs).anyMatch(Objects::nonNull); + if (isPrametrized) { + components.add(String.format("%08x", testDesc.testFullName().hashCode())); + } + + if (!components.isEmpty()) { + result = result.resolve(String.join(".", components)); + } + + return result; + } + + private enum Status { + Passed("[ OK ]"), + Failed("[ FAILED ]"), + Skipped("[ SKIPPED ]"); + + Status(String msg) { + this.msg = msg; + } + + @Override + public String toString() { + return msg; + } + + private final String msg; + } + + private int assertCount; + private Status status; + private RuntimeException skippedTestException; + private final TestDesc testDesc; + private final ThrowingFunction testConstructor; + private final ThrowingConsumer testBody; + private final List beforeActions; + private final List afterActions; + private final boolean dryRun; + private final Path workDir; + + private final static Set KEEP_WORK_DIR = Functional.identity( + () -> { + final String propertyName = "keep-work-dir"; + Set keepWorkDir = TKit.tokenizeConfigProperty( + propertyName); + if (keepWorkDir == null) { + return Set.of(Status.Failed); + } + + Predicate> isOneOf = options -> { + return !Collections.disjoint(keepWorkDir, options); + }; + + Set result = new HashSet<>(); + if (isOneOf.test(Set.of("pass", "p"))) { + result.add(Status.Passed); + } + if (isOneOf.test(Set.of("fail", "f"))) { + result.add(Status.Failed); + } + + return Collections.unmodifiableSet(result); + }).get(); + +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class WindowsHelper { + + static String getBundleName(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.WINDOWS); + return String.format("%s-%s%s", cmd.name(), cmd.version(), + cmd.packageType().getSuffix()); + } + + static Path getInstallationDirectory(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.WINDOWS); + Path installDir = Path.of( + cmd.getArgumentValue("--install-dir", () -> cmd.name())); + if (isUserLocalInstall(cmd)) { + return USER_LOCAL.resolve(installDir); + } + return PROGRAM_FILES.resolve(installDir); + } + + private static boolean isUserLocalInstall(JPackageCommand cmd) { + return cmd.hasArgument("--win-per-user-install"); + } + + static class AppVerifier { + + AppVerifier(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.WINDOWS); + this.cmd = cmd; + verifyStartMenuShortcut(); + verifyDesktopShortcut(); + verifyFileAssociationsRegistry(); + } + + private void verifyDesktopShortcut() { + boolean appInstalled = cmd.appLauncherPath().toFile().exists(); + if (cmd.hasArgument("--win-shortcut")) { + if (isUserLocalInstall(cmd)) { + verifyUserLocalDesktopShortcut(appInstalled); + verifySystemDesktopShortcut(false); + } else { + verifySystemDesktopShortcut(appInstalled); + verifyUserLocalDesktopShortcut(false); + } + } else { + verifySystemDesktopShortcut(false); + verifyUserLocalDesktopShortcut(false); + } + } + + private Path desktopShortcutPath() { + return Path.of(cmd.name() + ".lnk"); + } + + private void verifyShortcut(Path path, boolean exists) { + if (exists) { + TKit.assertFileExists(path); + } else { + TKit.assertPathExists(path, false); + } + } + + private void verifySystemDesktopShortcut(boolean exists) { + Path dir = Path.of(queryRegistryValueCache( + SYSTEM_SHELL_FOLDERS_REGKEY, "Common Desktop")); + verifyShortcut(dir.resolve(desktopShortcutPath()), exists); + } + + private void verifyUserLocalDesktopShortcut(boolean exists) { + Path dir = Path.of( + queryRegistryValueCache(USER_SHELL_FOLDERS_REGKEY, "Desktop")); + verifyShortcut(dir.resolve(desktopShortcutPath()), exists); + } + + private void verifyStartMenuShortcut() { + boolean appInstalled = cmd.appLauncherPath().toFile().exists(); + if (cmd.hasArgument("--win-menu")) { + if (isUserLocalInstall(cmd)) { + verifyUserLocalStartMenuShortcut(appInstalled); + verifySystemStartMenuShortcut(false); + } else { + verifySystemStartMenuShortcut(appInstalled); + verifyUserLocalStartMenuShortcut(false); + } + } else { + verifySystemStartMenuShortcut(false); + verifyUserLocalStartMenuShortcut(false); + } + } + + private Path startMenuShortcutPath() { + return Path.of(cmd.getArgumentValue("--win-menu-group", + () -> "Unknown"), cmd.name() + ".lnk"); + } + + private void verifyStartMenuShortcut(Path shortcutsRoot, boolean exists) { + Path shortcutPath = shortcutsRoot.resolve(startMenuShortcutPath()); + verifyShortcut(shortcutPath, exists); + if (!exists) { + TKit.assertPathExists(shortcutPath.getParent(), false); + } + } + + private void verifySystemStartMenuShortcut(boolean exists) { + verifyStartMenuShortcut(Path.of(queryRegistryValueCache( + SYSTEM_SHELL_FOLDERS_REGKEY, "Common Programs")), exists); + + } + + private void verifyUserLocalStartMenuShortcut(boolean exists) { + verifyStartMenuShortcut(Path.of(queryRegistryValueCache( + USER_SHELL_FOLDERS_REGKEY, "Programs")), exists); + } + + private void verifyFileAssociationsRegistry() { + Stream.of(cmd.getAllArgumentValues("--file-associations")).map( + Path::of).forEach(this::verifyFileAssociationsRegistry); + } + + private void verifyFileAssociationsRegistry(Path faFile) { + boolean appInstalled = cmd.appLauncherPath().toFile().exists(); + try { + TKit.trace(String.format( + "Get file association properties from [%s] file", + faFile)); + Map faProps = Files.readAllLines(faFile).stream().filter( + line -> line.trim().startsWith("extension=") || line.trim().startsWith( + "mime-type=")).map( + line -> { + String[] keyValue = line.trim().split("=", 2); + return Map.entry(keyValue[0], keyValue[1]); + }).collect(Collectors.toMap( + entry -> entry.getKey(), + entry -> entry.getValue())); + String suffix = faProps.get("extension"); + String contentType = faProps.get("mime-type"); + TKit.assertNotNull(suffix, String.format( + "Check file association suffix [%s] is found in [%s] property file", + suffix, faFile)); + TKit.assertNotNull(contentType, String.format( + "Check file association content type [%s] is found in [%s] property file", + contentType, faFile)); + verifyFileAssociations(appInstalled, "." + suffix, contentType); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void verifyFileAssociations(boolean exists, String suffix, + String contentType) { + String contentTypeFromRegistry = queryRegistryValue(Path.of( + "HKLM\\Software\\Classes", suffix).toString(), + "Content Type"); + String suffixFromRegistry = queryRegistryValue( + "HKLM\\Software\\Classes\\MIME\\Database\\Content Type\\" + contentType, + "Extension"); + + if (exists) { + TKit.assertEquals(suffix, suffixFromRegistry, + "Check suffix in registry is as expected"); + TKit.assertEquals(contentType, contentTypeFromRegistry, + "Check content type in registry is as expected"); + } else { + TKit.assertNull(suffixFromRegistry, + "Check suffix in registry not found"); + TKit.assertNull(contentTypeFromRegistry, + "Check content type in registry not found"); + } + } + + private final JPackageCommand cmd; + } + + private static String queryRegistryValue(String keyPath, String valueName) { + Executor.Result status = new Executor() + .setExecutable("reg") + .saveOutput() + .addArguments("query", keyPath, "/v", valueName) + .execute(); + if (status.exitCode == 1) { + // Should be the case of no such registry value or key + String lookupString = "ERROR: The system was unable to find the specified registry key or value."; + status.getOutput().stream().filter(line -> line.equals(lookupString)).findFirst().orElseThrow( + () -> new RuntimeException(String.format( + "Failed to find [%s] string in the output", + lookupString))); + TKit.trace(String.format( + "Registry value [%s] at [%s] path not found", valueName, + keyPath)); + return null; + } + + String value = status.assertExitCodeIsZero().getOutput().stream().skip(2).findFirst().orElseThrow(); + // Extract the last field from the following line: + // Common Desktop REG_SZ C:\Users\Public\Desktop + value = value.split(" REG_SZ ")[1]; + + TKit.trace(String.format("Registry value [%s] at [%s] path is [%s]", + valueName, keyPath, value)); + + return value; + } + + private static String queryRegistryValueCache(String keyPath, + String valueName) { + String key = String.format("[%s][%s]", keyPath, valueName); + String value = REGISTRY_VALUES.get(key); + if (value == null) { + value = queryRegistryValue(keyPath, valueName); + REGISTRY_VALUES.put(key, value); + } + + return value; + } + + static final Set CRITICAL_RUNTIME_FILES = Set.of(Path.of( + "bin\\server\\jvm.dll")); + + // jtreg resets %ProgramFiles% environment variable by some reason. + private final static Path PROGRAM_FILES = Path.of(Optional.ofNullable( + System.getenv("ProgramFiles")).orElse("C:\\Program Files")); + + private final static Path USER_LOCAL = Path.of(System.getProperty( + "user.home"), + "AppData", "Local"); + + private final static String SYSTEM_SHELL_FOLDERS_REGKEY = "HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders"; + private final static String USER_SHELL_FOLDERS_REGKEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders"; + + private static final Map REGISTRY_VALUES = new HashMap<>(); +} diff --git a/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/AppImageFileTest.java b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/AppImageFileTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/AppImageFileTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.LinkedHashMap; +import org.junit.Assert; +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +public class AppImageFileTest { + + @Rule + public final TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testIdentity() throws IOException { + Map params = new LinkedHashMap<>(); + params.put("name", "Foo"); + params.put("app-version", "2.3"); + params.put("description", "Duck is the King"); + AppImageFile aif = create(params); + + Assert.assertEquals("Foo", aif.getLauncherName()); + } + + @Test + public void testInvalidCommandLine() throws IOException { + // Just make sure AppImageFile will tolerate jpackage params that would + // never create app image at both load/save phases. + // People would edit this file just because they can. + // We should be ready to handle curious minds. + Map params = new LinkedHashMap<>(); + params.put("invalidParamName", "randomStringValue"); + create(params); + + params = new LinkedHashMap<>(); + params.put("name", "foo"); + params.put("app-version", ""); + create(params); + } + + @Test + public void testInavlidXml() throws IOException { + assertInvalid(createFromXml("")); + assertInvalid(createFromXml("")); + assertInvalid(createFromXml( + "", + "", + "")); + assertInvalid(createFromXml( + "", + "A", + "B", + "")); + } + + @Test + public void testValidXml() throws IOException { + Assert.assertEquals("Foo", (createFromXml( + "", + "Foo", + "")).getLauncherName()); + + Assert.assertEquals("Boo", (createFromXml( + "", + "Boo", + "Bar", + "")).getLauncherName()); + + var file = createFromXml( + "", + "Foo", + "", + ""); + Assert.assertEquals("Foo", file.getLauncherName()); + Assert.assertArrayEquals(new String[0], + file.getAddLauncherNames().toArray(String[]::new)); + } + + @Test + public void testMainLauncherName() throws IOException { + Map params = new LinkedHashMap<>(); + params.put("name", "Foo"); + params.put("description", "Duck App Description"); + AppImageFile aif = create(params); + + Assert.assertEquals("Foo", aif.getLauncherName()); + } + + @Test + public void testAddLauncherNames() throws IOException { + Map params = new LinkedHashMap<>(); + List> launchersAsMap = new ArrayList<>(); + + Map addLauncher2Params = new LinkedHashMap(); + addLauncher2Params.put("name", "Launcher2Name"); + launchersAsMap.add(addLauncher2Params); + + Map addLauncher3Params = new LinkedHashMap(); + addLauncher3Params.put("name", "Launcher3Name"); + launchersAsMap.add(addLauncher3Params); + + params.put("name", "Duke App"); + params.put("description", "Duke App Description"); + params.put("add-launcher", launchersAsMap); + AppImageFile aif = create(params); + + List addLauncherNames = aif.getAddLauncherNames(); + Assert.assertEquals(2, addLauncherNames.size()); + Assert.assertTrue(addLauncherNames.contains("Launcher2Name")); + Assert.assertTrue(addLauncherNames.contains("Launcher3Name")); + + } + + private AppImageFile create(Map params) throws IOException { + AppImageFile.save(tempFolder.getRoot().toPath(), params); + return AppImageFile.load(tempFolder.getRoot().toPath()); + } + + private void assertInvalid(AppImageFile file) { + Assert.assertNull(file.getLauncherName()); + Assert.assertNull(file.getAddLauncherNames()); + } + + private AppImageFile createFromXml(String... xmlData) throws IOException { + Path directory = tempFolder.getRoot().toPath(); + Path path = AppImageFile.getPathInAppImage(directory); + path.toFile().mkdirs(); + Files.delete(path); + + ArrayList data = new ArrayList(); + data.add(""); + data.addAll(List.of(xmlData)); + + Files.write(path, data, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + + AppImageFile image = AppImageFile.load(directory); + return image; + } + +} diff --git a/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/ApplicationLayoutTest.java b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/ApplicationLayoutTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/ApplicationLayoutTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; +import static org.junit.Assert.*; + + +public class ApplicationLayoutTest { + + @Rule + public final TemporaryFolder tempFolder = new TemporaryFolder(); + + private void fillLinuxAppImage() throws IOException { + appImage = tempFolder.newFolder("Foo").toPath(); + + Path base = appImage.getFileName(); + + tempFolder.newFolder(base.toString(), "bin"); + tempFolder.newFolder(base.toString(), "lib", "app", "mods"); + tempFolder.newFolder(base.toString(), "lib", "runtime", "bin"); + tempFolder.newFile(base.resolve("bin/Foo").toString()); + tempFolder.newFile(base.resolve("lib/app/Foo.cfg").toString()); + tempFolder.newFile(base.resolve("lib/app/hello.jar").toString()); + tempFolder.newFile(base.resolve("lib/Foo.png").toString()); + tempFolder.newFile(base.resolve("lib/libapplauncher.so").toString()); + tempFolder.newFile(base.resolve("lib/runtime/bin/java").toString()); + } + + @Test + public void testLinux() throws IOException { + fillLinuxAppImage(); + testApplicationLayout(ApplicationLayout.linuxAppImage()); + } + + private void testApplicationLayout(ApplicationLayout layout) throws IOException { + ApplicationLayout srcLayout = layout.resolveAt(appImage); + assertApplicationLayout(srcLayout); + + ApplicationLayout dstLayout = layout.resolveAt( + appImage.getParent().resolve( + "Copy" + appImage.getFileName().toString())); + srcLayout.move(dstLayout); + Files.deleteIfExists(appImage); + assertApplicationLayout(dstLayout); + + dstLayout.copy(srcLayout); + assertApplicationLayout(srcLayout); + assertApplicationLayout(dstLayout); + } + + private void assertApplicationLayout(ApplicationLayout layout) throws IOException { + assertTrue(Files.isRegularFile(layout.appDirectory().resolve("Foo.cfg"))); + assertTrue(Files.isRegularFile(layout.appDirectory().resolve("hello.jar"))); + assertTrue(Files.isDirectory(layout.appModsDirectory())); + assertTrue(Files.isRegularFile(layout.launchersDirectory().resolve("Foo"))); + assertTrue(Files.isRegularFile(layout.destktopIntegrationDirectory().resolve("Foo.png"))); + assertTrue(Files.isRegularFile(layout.dllDirectory().resolve("libapplauncher.so"))); + assertTrue(Files.isRegularFile(layout.runtimeDirectory().resolve("bin/java"))); + } + + private Path appImage; +} diff --git a/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/CompareDottedVersionTest.java b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/CompareDottedVersionTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/CompareDottedVersionTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import static org.junit.Assert.*; + +@RunWith(Parameterized.class) +public class CompareDottedVersionTest { + + public CompareDottedVersionTest(boolean greedy, String version1, + String version2, int result) { + this.version1 = version1; + this.version2 = version2; + this.expectedResult = result; + + if (greedy) { + createTestee = DottedVersion::greedy; + } else { + createTestee = DottedVersion::lazy; + } + } + + @Parameters + public static List data() { + List data = new ArrayList<>(); + for (var greedy : List.of(true, false)) { + data.addAll(List.of(new Object[][] { + { greedy, "00.0.0", "0", 0 }, + { greedy, "0.035", "0.0035", 0 }, + { greedy, "1", "1", 0 }, + { greedy, "2", "2.0", 0 }, + { greedy, "2.00", "2.0", 0 }, + { greedy, "1.2.3.4", "1.2.3.4.5", -1 }, + { greedy, "34", "33", 1 }, + { greedy, "34.0.78", "34.1.78", -1 } + })); + } + + data.addAll(List.of(new Object[][] { + { false, "", "1", -1 }, + { false, "1.2.4-R4", "1.2.4-R5", 0 }, + { false, "1.2.4.-R4", "1.2.4.R5", 0 }, + { false, "7+1", "7+4", 0 }, + { false, "2+14", "2-14", 0 }, + { false, "23.4.RC4", "23.3.RC10", 1 }, + { false, "77.0", "77.99999999999999999999999999999999999999999999999", 0 }, + })); + + return data; + } + + @Test + public void testIt() { + int actualResult = compare(version1, version2); + assertEquals(expectedResult, actualResult); + + int actualNegateResult = compare(version2, version1); + assertEquals(actualResult, -1 * actualNegateResult); + } + + private int compare(String x, String y) { + int result = createTestee.apply(x).compareTo(y); + + if (result < 0) { + return -1; + } + + if (result > 0) { + return 1; + } + + return 0; + } + + private final String version1; + private final String version2; + private final int expectedResult; + private final Function createTestee; +} diff --git a/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/DeployParamsTest.java b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/DeployParamsTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/DeployParamsTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.File; +import java.io.IOException; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.Rule; +import org.junit.Before; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; + +/** + * Test for JDK-8211285 + */ +public class DeployParamsTest { + + @Rule + public final TemporaryFolder tempFolder = new TemporaryFolder(); + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() throws IOException { + testRoot = tempFolder.newFolder(); + } + + @Test + public void testValidAppName() throws PackagerException { + initParamsAppName(); + + setAppNameAndValidate("Test"); + + setAppNameAndValidate("Test Name"); + + setAppNameAndValidate("Test - Name !!!"); + } + + @Test + public void testInvalidAppName() throws PackagerException { + initForInvalidAppNamePackagerException(); + initParamsAppName(); + setAppNameAndValidate("Test\nName"); + } + + @Test + public void testInvalidAppName2() throws PackagerException { + initForInvalidAppNamePackagerException(); + initParamsAppName(); + setAppNameAndValidate("Test\rName"); + } + + @Test + public void testInvalidAppName3() throws PackagerException { + initForInvalidAppNamePackagerException(); + initParamsAppName(); + setAppNameAndValidate("TestName\\"); + } + + @Test + public void testInvalidAppName4() throws PackagerException { + initForInvalidAppNamePackagerException(); + initParamsAppName(); + setAppNameAndValidate("Test \" Name"); + } + + private void initForInvalidAppNamePackagerException() { + thrown.expect(PackagerException.class); + + String msg = "Error: Invalid Application name"; + + // Unfortunately org.hamcrest.core.StringStartsWith is not available + // with older junit, DIY + + // thrown.expectMessage(startsWith("Error: Invalid Application name")); + thrown.expectMessage(new BaseMatcher() { + @Override + @SuppressWarnings("unchecked") + public boolean matches(Object o) { + if (o instanceof String) { + return ((String) o).startsWith(msg); + } + return false; + } + + @Override + public void describeTo(Description d) { + d.appendText(msg); + } + }); + } + + // Returns deploy params initialized to pass all validation, except for + // app name + private void initParamsAppName() { + params = new DeployParams(); + + params.setOutput(testRoot); + params.addResource(testRoot, new File(testRoot, "test.jar")); + params.addBundleArgument(Arguments.CLIOptions.APPCLASS.getId(), + "TestClass"); + params.addBundleArgument(Arguments.CLIOptions.MAIN_JAR.getId(), + "test.jar"); + params.addBundleArgument(Arguments.CLIOptions.INPUT.getId(), "input"); + } + + private void setAppNameAndValidate(String appName) throws PackagerException { + params.addBundleArgument(Arguments.CLIOptions.NAME.getId(), appName); + params.validate(); + } + + private File testRoot = null; + private DeployParams params; +} diff --git a/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/DottedVersionTest.java b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/DottedVersionTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/DottedVersionTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import static org.junit.Assert.*; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class DottedVersionTest { + + public DottedVersionTest(boolean greedy) { + this.greedy = greedy; + if (greedy) { + createTestee = DottedVersion::greedy; + } else { + createTestee = DottedVersion::lazy; + } + } + + @Parameterized.Parameters + public static List data() { + return List.of(new Object[] { true }, new Object[] { false }); + } + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + @Test + public void testValid() { + final List validStrings = List.of( + "1.0", + "1", + "2.234.045", + "2.234.0", + "0", + "0.1" + ); + + final List validLazyStrings; + if (greedy) { + validLazyStrings = Collections.emptyList(); + } else { + validLazyStrings = List.of( + "1.-1", + "5.", + "4.2.", + "3..2", + "2.a", + "0a", + ".", + " ", + " 1", + "1. 2", + "+1", + "-1", + "-0", + "1234567890123456789012345678901234567890" + ); + } + + Stream.concat(validStrings.stream(), validLazyStrings.stream()) + .forEach(value -> { + DottedVersion version = createTestee.apply(value); + assertEquals(version.toString(), value); + }); + } + + @Test + public void testNull() { + exceptionRule.expect(NullPointerException.class); + createTestee.apply(null); + } + + @Test + public void testEmpty() { + if (greedy) { + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage("Version may not be empty string"); + createTestee.apply(""); + } else { + assertTrue(0 == createTestee.apply("").compareTo("")); + assertTrue(0 == createTestee.apply("").compareTo("0")); + } + } + + private final boolean greedy; + private final Function createTestee; +} diff --git a/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/InvalidDottedVersionTest.java b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/InvalidDottedVersionTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/InvalidDottedVersionTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class InvalidDottedVersionTest { + + public InvalidDottedVersionTest(String version) { + this.version = version; + } + + @Parameters + public static List data() { + return Stream.of( + "1.-1", + "5.", + "4.2.", + "3..2", + "2.a", + "0a", + ".", + " ", + " 1", + "1. 2", + "+1", + "-1", + "-0", + "1234567890123456789012345678901234567890" + ).map(version -> new Object[] { version }).collect(Collectors.toList()); + } + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + @Test + public void testIt() { + exceptionRule.expect(IllegalArgumentException.class); + new DottedVersion(version); + } + + private final String version; +} diff --git a/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/OverridableResourceTest.java b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/OverridableResourceTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/OverridableResourceTest.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import jdk.incubator.jpackage.internal.resources.ResourceLocator; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class OverridableResourceTest { + + @Rule + public final TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testDefault() throws IOException { + byte[] actualBytes = saveToFile(new OverridableResource(DEFAULT_NAME)); + + try (InputStream is = ResourceLocator.class.getResourceAsStream( + DEFAULT_NAME)) { + assertArrayEquals(is.readAllBytes(), actualBytes); + } + } + + @Test + public void testDefaultWithSubstitution() throws IOException { + OverridableResource resource = new OverridableResource(DEFAULT_NAME); + + List linesBeforeSubstitution = convertToStringList(saveToFile( + resource)); + + if (SUBSTITUTION_DATA.size() != 1) { + // Test setup issue + throw new IllegalArgumentException( + "Substitution map should contain only a single entry"); + } + + resource.setSubstitutionData(SUBSTITUTION_DATA); + List linesAfterSubstitution = convertToStringList(saveToFile( + resource)); + + assertEquals(linesBeforeSubstitution.size(), linesAfterSubstitution.size()); + + Iterator beforeIt = linesBeforeSubstitution.iterator(); + Iterator afterIt = linesAfterSubstitution.iterator(); + + var substitutionEntry = SUBSTITUTION_DATA.entrySet().iterator().next(); + + boolean linesMismatch = false; + while (beforeIt.hasNext()) { + String beforeStr = beforeIt.next(); + String afterStr = afterIt.next(); + + if (beforeStr.equals(afterStr)) { + assertFalse(beforeStr.contains(substitutionEntry.getKey())); + } else { + linesMismatch = true; + assertTrue(beforeStr.contains(substitutionEntry.getKey())); + assertTrue(afterStr.contains(substitutionEntry.getValue())); + assertFalse(afterStr.contains(substitutionEntry.getKey())); + } + } + + assertTrue(linesMismatch); + } + + @Test + public void testCustom() throws IOException { + testCustom(DEFAULT_NAME); + } + + @Test + public void testCustomNoDefault() throws IOException { + testCustom(null); + } + + private void testCustom(String defaultName) throws IOException { + List expectedResourceData = List.of("A", "B", "C"); + + Path customFile = createCustomFile("foo", expectedResourceData); + + List actualResourceData = convertToStringList(saveToFile( + new OverridableResource(defaultName) + .setPublicName(customFile.getFileName()) + .setResourceDir(customFile.getParent()))); + + assertArrayEquals(expectedResourceData.toArray(String[]::new), + actualResourceData.toArray(String[]::new)); + } + + @Test + public void testCustomtWithSubstitution() throws IOException { + testCustomtWithSubstitution(DEFAULT_NAME); + } + + @Test + public void testCustomtWithSubstitutionNoDefault() throws IOException { + testCustomtWithSubstitution(null); + } + + private void testCustomtWithSubstitution(String defaultName) throws IOException { + final List resourceData = List.of("A", "[BB]", "C", "Foo", + "GoodbyeHello"); + final Path customFile = createCustomFile("foo", resourceData); + + final Map substitutionData = new HashMap(Map.of("B", + "Bar", "Foo", "B")); + substitutionData.put("Hello", null); + + final List expectedResourceData = List.of("A", "[BarBar]", "C", + "B", "Goodbye"); + + final List actualResourceData = convertToStringList(saveToFile( + new OverridableResource(defaultName) + .setPublicName(customFile.getFileName()) + .setSubstitutionData(substitutionData) + .setResourceDir(customFile.getParent()))); + assertArrayEquals(expectedResourceData.toArray(String[]::new), + actualResourceData.toArray(String[]::new)); + + // Don't call setPublicName() + final Path dstFile = tempFolder.newFolder().toPath().resolve(customFile.getFileName()); + new OverridableResource(defaultName) + .setSubstitutionData(substitutionData) + .setResourceDir(customFile.getParent()) + .saveToFile(dstFile); + assertArrayEquals(expectedResourceData.toArray(String[]::new), + convertToStringList(Files.readAllBytes(dstFile)).toArray( + String[]::new)); + + // Verify setSubstitutionData() stores a copy of passed in data + Map substitutionData2 = new HashMap(substitutionData); + var resource = new OverridableResource(defaultName) + .setResourceDir(customFile.getParent()); + + resource.setSubstitutionData(substitutionData2); + substitutionData2.clear(); + Files.delete(dstFile); + resource.saveToFile(dstFile); + assertArrayEquals(expectedResourceData.toArray(String[]::new), + convertToStringList(Files.readAllBytes(dstFile)).toArray( + String[]::new)); + } + + @Test + public void testNoDefault() throws IOException { + Path dstFolder = tempFolder.newFolder().toPath(); + Path dstFile = dstFolder.resolve(Path.of("foo", "bar")); + + new OverridableResource(null).saveToFile(dstFile); + + assertFalse(dstFile.toFile().exists()); + } + + private final static String DEFAULT_NAME; + private final static Map SUBSTITUTION_DATA; + static { + if (Platform.isWindows()) { + DEFAULT_NAME = "WinLauncher.template"; + SUBSTITUTION_DATA = Map.of("COMPANY_NAME", "Foo9090345"); + } else if (Platform.isLinux()) { + DEFAULT_NAME = "template.control"; + SUBSTITUTION_DATA = Map.of("APPLICATION_PACKAGE", "Package1967"); + } else if (Platform.isMac()) { + DEFAULT_NAME = "Info-lite.plist.template"; + SUBSTITUTION_DATA = Map.of("DEPLOY_BUNDLE_IDENTIFIER", "12345"); + } else { + throw Platform.throwUnknownPlatformError(); + } + } + + private byte[] saveToFile(OverridableResource resource) throws IOException { + Path dstFile = tempFolder.newFile().toPath(); + resource.saveToFile(dstFile); + assertThat(0, is(not(dstFile.toFile().length()))); + + return Files.readAllBytes(dstFile); + } + + private Path createCustomFile(String publicName, List data) throws + IOException { + Path resourceFolder = tempFolder.newFolder().toPath(); + Path customFile = resourceFolder.resolve(publicName); + + Files.write(customFile, data); + + return customFile; + } + + private static List convertToStringList(byte[] data) { + return List.of(new String(data, StandardCharsets.UTF_8).split("\\R")); + } +} diff --git a/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/PathGroupTest.java b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/PathGroupTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/PathGroupTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + + +public class PathGroupTest { + + @Rule + public final TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test(expected = NullPointerException.class) + public void testNullId() { + new PathGroup(Map.of()).getPath(null); + } + + @Test + public void testEmptyPathGroup() { + PathGroup pg = new PathGroup(Map.of()); + + assertNull(pg.getPath("foo")); + + assertEquals(0, pg.paths().size()); + assertEquals(0, pg.roots().size()); + } + + @Test + public void testRootsSinglePath() { + final PathGroup pg = new PathGroup(Map.of("main", PATH_FOO)); + + List paths = pg.paths(); + assertEquals(1, paths.size()); + assertEquals(PATH_FOO, paths.iterator().next()); + + List roots = pg.roots(); + assertEquals(1, roots.size()); + assertEquals(PATH_FOO, roots.iterator().next()); + } + + @Test + public void testDuplicatedRoots() { + final PathGroup pg = new PathGroup(Map.of("main", PATH_FOO, "another", + PATH_FOO, "root", PATH_EMPTY)); + + List paths = pg.paths(); + Collections.sort(paths); + + assertEquals(3, paths.size()); + assertEquals(PATH_EMPTY, paths.get(0)); + assertEquals(PATH_FOO, paths.get(1)); + assertEquals(PATH_FOO, paths.get(2)); + + List roots = pg.roots(); + assertEquals(1, roots.size()); + assertEquals(PATH_EMPTY, roots.get(0)); + } + + @Test + public void testRoots() { + final PathGroup pg = new PathGroup(Map.of(1, Path.of("foo"), 2, Path.of( + "foo", "bar"), 3, Path.of("foo", "bar", "buz"))); + + List paths = pg.paths(); + assertEquals(3, paths.size()); + assertTrue(paths.contains(Path.of("foo"))); + assertTrue(paths.contains(Path.of("foo", "bar"))); + assertTrue(paths.contains(Path.of("foo", "bar", "buz"))); + + List roots = pg.roots(); + assertEquals(1, roots.size()); + assertEquals(Path.of("foo"), roots.get(0)); + } + + @Test + public void testResolveAt() { + final PathGroup pg = new PathGroup(Map.of(0, PATH_FOO, 1, PATH_BAR, 2, + PATH_EMPTY)); + + final Path aPath = Path.of("a"); + + final PathGroup pg2 = pg.resolveAt(aPath); + assertThat(pg, not(equalTo(pg2))); + + List paths = pg.paths(); + assertEquals(3, paths.size()); + assertTrue(paths.contains(PATH_EMPTY)); + assertTrue(paths.contains(PATH_FOO)); + assertTrue(paths.contains(PATH_BAR)); + assertEquals(PATH_EMPTY, pg.roots().get(0)); + + paths = pg2.paths(); + assertEquals(3, paths.size()); + assertTrue(paths.contains(aPath.resolve(PATH_EMPTY))); + assertTrue(paths.contains(aPath.resolve(PATH_FOO))); + assertTrue(paths.contains(aPath.resolve(PATH_BAR))); + assertEquals(aPath, pg2.roots().get(0)); + } + + @Test + public void testTransform() throws IOException { + for (var transform : TransformType.values()) { + testTransform(false, transform); + } + } + + @Test + public void testTransformWithExcludes() throws IOException { + for (var transform : TransformType.values()) { + testTransform(true, transform); + } + } + + enum TransformType { Copy, Move, Handler }; + + private void testTransform(boolean withExcludes, TransformType transform) + throws IOException { + final PathGroup pg = new PathGroup(Map.of(0, PATH_FOO, 1, PATH_BAR, 2, + PATH_EMPTY, 3, PATH_BAZ)); + + final Path srcDir = tempFolder.newFolder().toPath(); + final Path dstDir = tempFolder.newFolder().toPath(); + + Files.createDirectories(srcDir.resolve(PATH_FOO).resolve("a/b/c/d")); + Files.createFile(srcDir.resolve(PATH_FOO).resolve("a/b/c/file1")); + Files.createFile(srcDir.resolve(PATH_FOO).resolve("a/b/file2")); + Files.createFile(srcDir.resolve(PATH_FOO).resolve("a/b/file3")); + Files.createFile(srcDir.resolve(PATH_BAR)); + Files.createFile(srcDir.resolve(PATH_EMPTY).resolve("file4")); + Files.createDirectories(srcDir.resolve(PATH_BAZ).resolve("1/2/3")); + + var dst = pg.resolveAt(dstDir); + var src = pg.resolveAt(srcDir); + if (withExcludes) { + // Exclude from transformation. + src.setPath(new Object(), srcDir.resolve(PATH_FOO).resolve("a/b/c")); + src.setPath(new Object(), srcDir.resolve(PATH_EMPTY).resolve("file4")); + } + + var srcFilesBeforeTransform = walkFiles(srcDir); + + if (transform == TransformType.Handler) { + List> copyFile = new ArrayList<>(); + List createDirectory = new ArrayList<>(); + src.transform(dst, new PathGroup.TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + copyFile.add(Map.entry(src, dst)); + } + + @Override + public void createDirectory(Path dir) throws IOException { + createDirectory.add(dir); + } + }); + + Consumer assertFile = path -> { + var entry = Map.entry(srcDir.resolve(path), dstDir.resolve(path)); + assertTrue(copyFile.contains(entry)); + }; + + Consumer assertDir = path -> { + assertTrue(createDirectory.contains(dstDir.resolve(path))); + }; + + assertEquals(withExcludes ? 3 : 5, copyFile.size()); + assertEquals(withExcludes ? 8 : 10, createDirectory.size()); + + assertFile.accept(PATH_FOO.resolve("a/b/file2")); + assertFile.accept(PATH_FOO.resolve("a/b/file3")); + assertFile.accept(PATH_BAR); + assertDir.accept(PATH_FOO.resolve("a/b")); + assertDir.accept(PATH_FOO.resolve("a")); + assertDir.accept(PATH_FOO); + assertDir.accept(PATH_BAZ); + assertDir.accept(PATH_BAZ.resolve("1")); + assertDir.accept(PATH_BAZ.resolve("1/2")); + assertDir.accept(PATH_BAZ.resolve("1/2/3")); + assertDir.accept(PATH_EMPTY); + + if (!withExcludes) { + assertFile.accept(PATH_FOO.resolve("a/b/c/file1")); + assertFile.accept(PATH_EMPTY.resolve("file4")); + assertDir.accept(PATH_FOO.resolve("a/b/c/d")); + assertDir.accept(PATH_FOO.resolve("a/b/c")); + } + + assertArrayEquals(new Path[] { Path.of("") }, walkFiles(dstDir)); + return; + } + + if (transform == TransformType.Copy) { + src.copy(dst); + } else if (transform == TransformType.Move) { + src.move(dst); + } + + final List excludedPaths; + if (withExcludes) { + excludedPaths = List.of( + PATH_EMPTY.resolve("file4"), + PATH_FOO.resolve("a/b/c") + ); + } else { + excludedPaths = Collections.emptyList(); + } + UnaryOperator removeExcludes = paths -> { + return Stream.of(paths) + .filter(path -> !excludedPaths.stream().anyMatch( + path::startsWith)) + .collect(Collectors.toList()).toArray(Path[]::new); + }; + + var dstFiles = walkFiles(dstDir); + assertArrayEquals(removeExcludes.apply(srcFilesBeforeTransform), dstFiles); + + if (transform == TransformType.Copy) { + assertArrayEquals(dstFiles, removeExcludes.apply(walkFiles(srcDir))); + } else if (transform == TransformType.Move) { + assertFalse(Files.exists(srcDir)); + } + } + + private static Path[] walkFiles(Path root) throws IOException { + try (var files = Files.walk(root)) { + return files.map(root::relativize).sorted().collect( + Collectors.toList()).toArray(Path[]::new); + } + } + + private final static Path PATH_FOO = Path.of("foo"); + private final static Path PATH_BAR = Path.of("bar"); + private final static Path PATH_BAZ = Path.of("baz"); + private final static Path PATH_EMPTY = Path.of(""); +} diff --git a/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/ToolValidatorTest.java b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/ToolValidatorTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/ToolValidatorTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.incubator.jpackage.internal; + +import java.nio.file.Path; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.*; +import org.junit.Test; + + +public class ToolValidatorTest { + + @Test + public void testAvailable() { + assertNull(new ToolValidator(TOOL_JAVA).validate()); + } + + @Test + public void testNotAvailable() { + assertValidationFailure(new ToolValidator(TOOL_UNKNOWN).validate(), true); + } + + @Test + public void testVersionParserUsage() { + // Without minimal version configured, version parser should not be used + new ToolValidator(TOOL_JAVA).setVersionParser(unused -> { + throw new RuntimeException(); + }).validate(); + + // Minimal version is 1, actual is 10. Should be OK. + assertNull(new ToolValidator(TOOL_JAVA).setMinimalVersion( + new DottedVersion("1")).setVersionParser(unused -> "10").validate()); + + // Minimal version is 5, actual is 4.99.37. Error expected. + assertValidationFailure(new ToolValidator(TOOL_JAVA).setMinimalVersion( + new DottedVersion("5")).setVersionParser(unused -> "4.99.37").validate(), + false); + + // Minimal version is 8, actual is 10, lexicographical comparison is used. Error expected. + assertValidationFailure(new ToolValidator(TOOL_JAVA).setMinimalVersion( + "8").setVersionParser(unused -> "10").validate(), false); + + // Minimal version is 8, actual is 10, Use DottedVersion class for comparison. Should be OK. + assertNull(new ToolValidator(TOOL_JAVA).setMinimalVersion( + new DottedVersion("8")).setVersionParser(unused -> "10").validate()); + } + + private static void assertValidationFailure(ConfigException v, + boolean withCause) { + assertNotNull(v); + assertThat("", is(not(v.getMessage().strip()))); + assertThat("", is(not(v.advice.strip()))); + if (withCause) { + assertNotNull(v.getCause()); + } else { + assertNull(v.getCause()); + } + } + + private final static String TOOL_JAVA; + private final static String TOOL_UNKNOWN = Path.of(System.getProperty( + "java.home"), "bin").toString(); + + static { + String fname = "java"; + if (Platform.isWindows()) { + fname = fname + ".exe"; + } + TOOL_JAVA = Path.of(System.getProperty("java.home"), "bin", fname).toString(); + } +} diff --git a/test/jdk/tools/jpackage/junit/junit.java b/test/jdk/tools/jpackage/junit/junit.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/junit.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary jpackage unit tests + * @library ${jtreg.home}/lib/junit.jar + * @run shell run_junit.sh + */ diff --git a/test/jdk/tools/jpackage/junit/run_junit.sh b/test/jdk/tools/jpackage/junit/run_junit.sh new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/run_junit.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -x + +set -e +if [ -z "$BASH" ]; then + # The script relies on Bash arrays, rerun in Bash. + /bin/bash $0 $@ + exit +fi + +sources=() +classes=() +for s in $(find "${TESTSRC}" -name "*.java" | grep -v junit.java); do + sources+=( "$s" ) + classes+=( $(echo "$s" | sed -e "s|${TESTSRC}/||" -e 's|/|.|g' -e 's/.java$//') ) +done + +common_args=(\ + --add-modules jdk.incubator.jpackage \ + --patch-module jdk.incubator.jpackage="${TESTSRC}${PS}${TESTCLASSES}" \ + --add-reads jdk.incubator.jpackage=ALL-UNNAMED \ + --add-exports jdk.incubator.jpackage/jdk.incubator.jpackage.internal=ALL-UNNAMED \ + -classpath "${TESTCLASSPATH}" \ +) + +# Compile classes for junit +"${COMPILEJAVA}/bin/javac" ${TESTTOOLVMOPTS} ${TESTJAVACOPTS} \ + "${common_args[@]}" -d "${TESTCLASSES}" "${sources[@]}" + +# Run junit +"${TESTJAVA}/bin/java" ${TESTVMOPTS} ${TESTJAVAOPTS} \ + "${common_args[@]}" org.junit.runner.JUnitCore "${classes[@]}" diff --git a/test/jdk/tools/jpackage/linux/AppCategoryTest.java b/test/jdk/tools/jpackage/linux/AppCategoryTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/linux/AppCategoryTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; + + +/** + * Test --linux-app-category parameter. Output of the test should be + * appcategorytest_1.0-1_amd64.deb or appcategorytest-1.0-1.amd64.rpm package + * bundle. The output package should provide the same functionality as the + * default package. + * + * deb: + * Section property of the package should be set to Foo value. + * + * rpm: + * Group property of the package should be set to Foo value. + */ + + +/* + * @test + * @summary jpackage with --linux-app-category + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "linux") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @run main/othervm/timeout=360 -Xmx512m AppCategoryTest + */ +public class AppCategoryTest { + + public static void main(String[] args) { + final String CATEGORY = "Foo"; + + TKit.run(args, () -> { + new PackageTest() + .forTypes(PackageType.LINUX) + .configureHelloApp() + .addInitializer(cmd -> { + cmd.addArguments("--linux-app-category", CATEGORY); + }) + .forTypes(PackageType.LINUX_DEB) + .addBundlePropertyVerifier("Section", CATEGORY) + .forTypes(PackageType.LINUX_RPM) + .addBundlePropertyVerifier("Group", CATEGORY) + .run(); + }); + } +} diff --git a/test/jdk/tools/jpackage/linux/LicenseTypeTest.java b/test/jdk/tools/jpackage/linux/LicenseTypeTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/linux/LicenseTypeTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; + + +/** + * Test --linux-rpm-license-type parameter. Output of the test should be + * licensetypetest-1.0-1.amd64.rpm package bundle. The output package + * should provide the same functionality as the + * default package. + * License property of the package should be set to JP_LICENSE_TYPE. + */ + + +/* + * @test + * @summary jpackage with --linux-rpm-license-type + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "linux") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @run main/othervm/timeout=360 -Xmx512m LicenseTypeTest + */ +public class LicenseTypeTest { + + public static void main(String[] args) { + final String LICENSE_TYPE = "JP_LICENSE_TYPE"; + + TKit.run(args, () -> { + new PackageTest().forTypes(PackageType.LINUX_RPM).configureHelloApp() + .addInitializer(cmd -> { + cmd.addArguments("--linux-rpm-license-type", LICENSE_TYPE); + }) + .addBundlePropertyVerifier("License", LICENSE_TYPE) + .run(); + }); + } +} diff --git a/test/jdk/tools/jpackage/linux/LinuxBundleNameTest.java b/test/jdk/tools/jpackage/linux/LinuxBundleNameTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/linux/LinuxBundleNameTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; + + +/** + * Test --linux-package-name parameter. Output of the test should be + * quickbrownfox2_1.0-1_amd64.deb or quickbrownfox2-1.0-1.amd64.rpm package + * bundle. The output package should provide the same functionality as the + * default package. + * + * deb: + * Package property of the package should be set to quickbrownfox2. + * + * rpm: + * Name property of the package should be set to quickbrownfox2. + */ + + +/* + * @test + * @summary jpackage with --linux-package-name + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "linux") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @run main/othervm/timeout=360 -Xmx512m LinuxBundleNameTest + */ +public class LinuxBundleNameTest { + + public static void main(String[] args) { + final String PACKAGE_NAME = "quickbrownfox2"; + + TKit.run(args, () -> { + new PackageTest() + .forTypes(PackageType.LINUX) + .configureHelloApp() + .addInitializer(cmd -> { + cmd.addArguments("--linux-package-name", PACKAGE_NAME); + }) + .forTypes(PackageType.LINUX_DEB) + .addBundlePropertyVerifier("Package", PACKAGE_NAME) + .forTypes(PackageType.LINUX_RPM) + .addBundlePropertyVerifier("Name", PACKAGE_NAME) + .run(); + }); + } +} diff --git a/test/jdk/tools/jpackage/linux/LinuxResourceTest.java b/test/jdk/tools/jpackage/linux/LinuxResourceTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/linux/LinuxResourceTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.nio.file.Path; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.LinuxHelper; +import jdk.jpackage.test.Annotations.Test; +import java.util.List; + +/* + * @test + * @summary jpackage with --resource-dir + * @library ../helpers + * @build jdk.jpackage.test.* + * @requires (os.family == "linux") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile LinuxResourceTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=LinuxResourceTest + */ + +public class LinuxResourceTest { + @Test + public static void testHardcodedProperties() throws IOException { + new PackageTest() + .forTypes(PackageType.LINUX) + .configureHelloApp() + .addInitializer(cmd -> { + cmd + .setFakeRuntime() + .saveConsoleOutput(true) + .addArguments("--resource-dir", TKit.createTempDirectory("resources")); + }) + .forTypes(PackageType.LINUX_DEB) + .addInitializer(cmd -> { + Path controlFile = Path.of(cmd.getArgumentValue("--resource-dir"), + "control"); + TKit.createTextFile(controlFile, List.of( + "Package: dont-install-me", + "Version: 1.2.3-R2", + "Section: APPLICATION_SECTION", + "Maintainer: APPLICATION_MAINTAINER", + "Priority: optional", + "Architecture: bar", + "Provides: dont-install-me", + "Description: APPLICATION_DESCRIPTION", + "Installed-Size: APPLICATION_INSTALLED_SIZE", + "Depends: PACKAGE_DEFAULT_DEPENDENCIES" + )); + }) + .addBundleVerifier((cmd, result) -> { + TKit.assertTextStream("Using custom package resource [DEB control file]") + .predicate(String::startsWith) + .apply(result.getOutput().stream()); + TKit.assertTextStream(String.format( + "Expected value of \"Package\" property is [%s]. Actual value in output package is [dont-install-me]", + LinuxHelper.getPackageName(cmd))) + .predicate(String::startsWith) + .apply(result.getOutput().stream()); + TKit.assertTextStream( + "Expected value of \"Version\" property is [1.0-1]. Actual value in output package is [1.2.3-R2]") + .predicate(String::startsWith) + .apply(result.getOutput().stream()); + TKit.assertTextStream(String.format( + "Expected value of \"Architecture\" property is [%s]. Actual value in output package is [bar]", + LinuxHelper.getDefaultPackageArch(cmd.packageType()))) + .predicate(String::startsWith) + .apply(result.getOutput().stream()); + }) + .forTypes(PackageType.LINUX_RPM) + .addInitializer(cmd -> { + Path specFile = Path.of(cmd.getArgumentValue("--resource-dir"), + LinuxHelper.getPackageName(cmd) + ".spec"); + TKit.createTextFile(specFile, List.of( + "Name: dont-install-me", + "Version: 1.2.3", + "Release: R2", + "Summary: APPLICATION_SUMMARY", + "License: APPLICATION_LICENSE_TYPE", + "Prefix: %{dirname:APPLICATION_DIRECTORY}", + "Provides: dont-install-me", + "%description", + "APPLICATION_DESCRIPTION", + "%prep", + "%build", + "%install", + "rm -rf %{buildroot}", + "install -d -m 755 %{buildroot}APPLICATION_DIRECTORY", + "cp -r %{_sourcedir}APPLICATION_DIRECTORY/* %{buildroot}APPLICATION_DIRECTORY", + "%files", + "APPLICATION_DIRECTORY" + )); + }) + .addBundleVerifier((cmd, result) -> { + TKit.assertTextStream("Using custom package resource [RPM spec file]") + .predicate(String::startsWith) + .apply(result.getOutput().stream()); + TKit.assertTextStream(String.format( + "Expected value of \"Name\" property is [%s]. Actual value in output package is [dont-install-me]", + LinuxHelper.getPackageName(cmd))) + .predicate(String::startsWith) + .apply(result.getOutput().stream()); + TKit.assertTextStream( + "Expected value of \"Version\" property is [1.0]. Actual value in output package is [1.2.3]") + .predicate(String::startsWith) + .apply(result.getOutput().stream()); + TKit.assertTextStream( + "Expected value of \"Release\" property is [1]. Actual value in output package is [R2]") + .predicate(String::startsWith) + .apply(result.getOutput().stream()); + }) + .run(); + } +} diff --git a/test/jdk/tools/jpackage/linux/MaintainerTest.java b/test/jdk/tools/jpackage/linux/MaintainerTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/linux/MaintainerTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.TKit; + + +/** + * Test --linux-deb-maintainer parameter. Output of the test should be + * maintainertest_1.0-1_amd64.deb package bundle. The output package + * should provide the same functionality as the + * default package. + * Value of Maintainer property of the package should contain + * jpackage-test@java.com email address. + */ + + +/* + * @test + * @summary jpackage with --linux-deb-maintainer + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "linux") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @run main/othervm/timeout=360 -Xmx512m MaintainerTest + */ +public class MaintainerTest { + + public static void main(String[] args) { + final String MAINTAINER = "jpackage-test@java.com"; + + TKit.run(args, () -> { + new PackageTest().forTypes(PackageType.LINUX_DEB).configureHelloApp() + .addInitializer(cmd -> { + cmd.addArguments("--linux-deb-maintainer", MAINTAINER); + }) + .addBundlePropertyVerifier("Maintainer", (propName, propValue) -> { + String lookupValue = "<" + MAINTAINER + ">"; + TKit.assertTrue(propValue.endsWith(lookupValue), + String.format("Check value of %s property [%s] ends with %s", + propName, propValue, lookupValue)); + }) + .run(); + }); + } +} diff --git a/test/jdk/tools/jpackage/linux/PackageDepsTest.java b/test/jdk/tools/jpackage/linux/PackageDepsTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/linux/PackageDepsTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.LinuxHelper; + + +/** + * Test --linux-package-deps parameter. Output of the test should be + * apackagedepstestprereq_1.0-1_amd64.deb and packagedepstest_1.0-1_amd64.deb or + * apackagedepstestprereq-1.0-1.amd64.rpm and packagedepstest-1.0-1.amd64.rpm + * package bundles. The output packages should provide the same functionality as + * the default package. + * + * deb: Value of Depends property of packagedepstest package should contain + * apackagedepstestprereq word. + * + * rpm: Value of Requires property of packagedepstest package should contain + * apackagedepstestprereq word. + */ + + +/* + * @test + * @summary jpackage with --linux-package-deps + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "linux") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @run main/othervm/timeout=360 -Xmx512m PackageDepsTest + */ +public class PackageDepsTest { + + public static void main(String[] args) { + // Pick the name of prerequisite package to be alphabetically + // preceeding the main package name. + // This is needed to make Bash script batch installing/uninstalling packages + // produced by jtreg tests install/uninstall packages in the right order. + final String PREREQ_PACKAGE_NAME = "apackagedepstestprereq"; + + TKit.run(args, () -> { + new PackageTest() + .forTypes(PackageType.LINUX) + .configureHelloApp() + .addInitializer(cmd -> { + cmd.setArgumentValue("--name", PREREQ_PACKAGE_NAME); + }) + .run(); + + new PackageTest() + .forTypes(PackageType.LINUX) + .configureHelloApp() + .addInitializer(cmd -> { + cmd.addArguments("--linux-package-deps", PREREQ_PACKAGE_NAME); + }) + .forTypes(PackageType.LINUX) + .addBundleVerifier(cmd -> { + TKit.assertTrue( + LinuxHelper.getPrerequisitePackages(cmd).contains( + PREREQ_PACKAGE_NAME), String.format( + "Check package depends on [%s] package", + PREREQ_PACKAGE_NAME)); + }) + .run(); + }); + } +} diff --git a/test/jdk/tools/jpackage/linux/ReleaseTest.java b/test/jdk/tools/jpackage/linux/ReleaseTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/linux/ReleaseTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.TKit; + + +/** + * Test --linux-app-release parameter. Output of the test should be + * releasetest_1.0-Rc3_amd64.deb or releasetest-1.0-Rc3.amd64.rpm package + * bundle. The output package should provide the same functionality as the + * default package. + * + * deb: + * Version property of the package should end with -Rc3 substring. + * + * rpm: + * Release property of the package should be set to Rc3 value. + */ + +/* + * @test + * @summary jpackage with --linux-app-release + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "linux") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @run main/othervm/timeout=360 -Xmx512m ReleaseTest + */ +public class ReleaseTest { + + public static void main(String[] args) { + final String RELEASE = "Rc3"; + + TKit.run(args, () -> { + new PackageTest() + .forTypes(PackageType.LINUX) + .configureHelloApp() + .addInitializer(cmd -> { + cmd.addArguments("--linux-app-release", RELEASE); + }) + .forTypes(PackageType.LINUX_RPM) + .addBundlePropertyVerifier("Release", RELEASE) + .forTypes(PackageType.LINUX_DEB) + .addBundlePropertyVerifier("Version", (propName, propValue) -> { + TKit.assertTrue(propValue.endsWith("-" + RELEASE), + String.format("Check value of %s property [%s] ends with %s", + propName, propValue, RELEASE)); + }) + .run(); + }); + } +} diff --git a/test/jdk/tools/jpackage/linux/ShortcutHintTest.java b/test/jdk/tools/jpackage/linux/ShortcutHintTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/linux/ShortcutHintTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Map; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; +import jdk.jpackage.test.FileAssociations; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.*; + +/** + * Test --linux-shortcut parameter. Output of the test should be + * shortcuthinttest_1.0-1_amd64.deb or shortcuthinttest-1.0-1.amd64.rpm package + * bundle. The output package should provide the same functionality as the + * default package and also create a desktop shortcut. + * + * Finding a shortcut of the application launcher through GUI depends on desktop + * environment. + * + * deb: + * Search online for `Ways To Open A Ubuntu Application` for instructions. + * + * rpm: + * + */ + +/* + * @test + * @summary jpackage with --linux-shortcut + * @library ../helpers + * @key jpackagePlatformPackage + * @requires jpackage.test.SQETest == null + * @build jdk.jpackage.test.* + * @requires (os.family == "linux") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile ShortcutHintTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=ShortcutHintTest + */ + +/* + * @test + * @summary jpackage with --linux-shortcut + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "linux") + * @requires jpackage.test.SQETest != null + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile ShortcutHintTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=ShortcutHintTest.testBasic + */ + +public class ShortcutHintTest { + + @Test + public static void testBasic() { + createTest().addInitializer(cmd -> { + cmd.addArgument("--linux-shortcut"); + }).run(); + } + + private static PackageTest createTest() { + return new PackageTest() + .forTypes(PackageType.LINUX) + .configureHelloApp() + .addBundleDesktopIntegrationVerifier(true); + + } + + /** + * Adding `--icon` to jpackage command line should create desktop shortcut + * even though `--linux-shortcut` is omitted. + */ + @Test + public static void testCustomIcon() { + createTest().addInitializer(cmd -> { + cmd.setFakeRuntime(); + cmd.addArguments("--icon", TKit.TEST_SRC_ROOT.resolve( + "apps/dukeplug.png")); + }).run(); + } + + /** + * Adding `--file-associations` to jpackage command line should create + * desktop shortcut even though `--linux-shortcut` is omitted. + */ + @Test + public static void testFileAssociations() { + PackageTest test = createTest().addInitializer( + JPackageCommand::setFakeRuntime); + new FileAssociations("ShortcutHintTest_testFileAssociations").applyTo( + test); + test.run(); + } + + /** + * Additional launcher with icon should create desktop shortcut even though + * `--linux-shortcut` is omitted. + */ + @Test + public static void testAdditionaltLaunchers() { + createTest().addInitializer(cmd -> { + cmd.setFakeRuntime(); + + final String launcherName = "Foo"; + final Path propsFile = TKit.workDir().resolve( + launcherName + ".properties"); + + cmd.addArguments("--add-launcher", String.format("%s=%s", + launcherName, propsFile)); + + TKit.createPropertiesFile(propsFile, Map.entry("icon", + TKit.TEST_SRC_ROOT.resolve("apps/dukeplug.png").toString())); + }).run(); + } + + /** + * .desktop file from resource dir. + */ + @Test + public static void testDesktopFileFromResourceDir() { + final String expectedVersionString = "Version=12345678"; + TKit.withTempDirectory("resources", tempDir -> { + createTest().addInitializer(cmd -> { + cmd.setFakeRuntime(); + + cmd.addArgument("--linux-shortcut"); + cmd.addArguments("--resource-dir", tempDir); + + // Create custom .desktop file in resource directory + TKit.createTextFile(tempDir.resolve(cmd.name() + ".desktop"), + List.of( + "[Desktop Entry]", + "Name=APPLICATION_NAME", + "Exec=APPLICATION_LAUNCHER", + "Terminal=false", + "Type=Application", + "Categories=DEPLOY_BUNDLE_CATEGORY", + expectedVersionString + )); + }) + .addInstallVerifier(cmd -> { + Path desktopFile = cmd.appLayout().destktopIntegrationDirectory().resolve( + String.format("%s-%s.desktop", + LinuxHelper.getPackageName(cmd), cmd.name())); + TKit.assertFileExists(desktopFile); + TKit.assertTextStream(expectedVersionString) + .label(String.format("[%s] file", desktopFile)) + .predicate(String::equals) + .apply(Files.readAllLines(desktopFile).stream()); + }).run(); + }); + } +} diff --git a/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java b/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.MacHelper; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.Annotations.Parameter; + + +/** + * Test --mac-package-name, --mac-package-identifier parameters. + */ + +/* + * @test + * @summary jpackage with --mac-package-name, --mac-package-identifier + * @library ../helpers + * @build jdk.jpackage.test.* + * @requires (os.family == "mac") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile MacPropertiesTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=MacPropertiesTest + */ +public class MacPropertiesTest { + @Test + @Parameter("MacPackageNameTest") + public void testPackageName(String packageName) { + testParameterInAppImage("--mac-package-name", "CFBundleName", + packageName); + } + + @Test + @Parameter("Foo") + public void testPackageIdetifier(String packageId) { + testParameterInAppImage("--mac-package-identifier", "CFBundleIdentifier", + packageId); + } + + private static void testParameterInAppImage(String jpackageParameterName, + String plistKeyName, String value) { + JPackageCommand cmd = JPackageCommand.helloAppImage() + .addArguments(jpackageParameterName, value); + + cmd.executeAndAssertHelloAppImageCreated(); + + var plist = MacHelper.readPListFromAppImage(cmd.outputBundle()); + + TKit.assertEquals(value, plist.queryValue(plistKeyName), String.format( + "Check value of %s plist key", plistKeyName)); + } +} diff --git a/test/jdk/tools/jpackage/macosx/NameWithSpaceTest.java b/test/jdk/tools/jpackage/macosx/NameWithSpaceTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/macosx/NameWithSpaceTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.Annotations.Test; + +/** + * Name with space packaging test. Output of the test should be + * "Name With Space-*.*" package bundle. + * + * macOS only: + * + * Test should generates basic pkg and dmg. Name of packages and application itself + * should have name: "Name With Space". Package should be installed into "/Applications" + * folder and verified that it can be installed and run. + */ + +/* + * @test + * @summary jpackage test with name containing spaces + * @library ../helpers + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile NameWithSpaceTest.java + * @requires (os.family == "mac") + * @key jpackagePlatformPackage + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=NameWithSpaceTest + */ +public class NameWithSpaceTest { + + @Test + public static void test() { + new PackageTest() + .configureHelloApp() + .addBundleDesktopIntegrationVerifier(false) + .addInitializer(cmd -> { + cmd.setArgumentValue("--name", "Name With Space"); + }) + .run(); + } +} diff --git a/test/jdk/tools/jpackage/macosx/SigningAppImageTest.java b/test/jdk/tools/jpackage/macosx/SigningAppImageTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/macosx/SigningAppImageTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.TKit; + +/** + * Tests generation of app image with --mac-sign and related arguments. Test will + * generate app image and verify signature of main launcher and app bundle itself. + * This test requires that machine is configured with test certificate for + * "Developer ID Application: jpackage.openjdk.java.net" in jpackagerTest keychain with + * always allowed access to this keychain for user which runs test. + */ + +/* + * @test + * @summary jpackage with --type app-image --mac-sign + * @library ../helpers + * @library /test/lib + * @library base + * @build SigningBase + * @build SigningCheck + * @build jtreg.SkippedException + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @requires (os.family == "mac") + * @run main/othervm -Xmx512m SigningAppImageTest + */ +public class SigningAppImageTest { + + public static void main(String[] args) throws Exception { + TKit.run(args, () -> { + SigningCheck.checkCertificates(); + + JPackageCommand cmd = JPackageCommand.helloAppImage(); + cmd.addArguments("--mac-sign", "--mac-signing-key-user-name", + SigningBase.DEV_NAME, "--mac-signing-keychain", + "jpackagerTest.keychain"); + cmd.executeAndAssertHelloAppImageCreated(); + + Path launcherPath = cmd.appLauncherPath(); + SigningBase.verifyCodesign(launcherPath, true); + + Path appImage = cmd.outputBundle(); + SigningBase.verifyCodesign(appImage, true); + SigningBase.verifySpctl(appImage, "exec"); + }); + } +} diff --git a/test/jdk/tools/jpackage/macosx/SigningPackageTest.java b/test/jdk/tools/jpackage/macosx/SigningPackageTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/macosx/SigningPackageTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import java.nio.file.Paths; +import jdk.jpackage.test.*; + +/** + * Tests generation of dmg and pkg with --mac-sign and related arguments. Test will + * generate pkg and verifies its signature. It verifies that dmg is not signed, but app + * image inside dmg is signed. This test requires that machine is configured with test + * certificate for "Developer ID Installer: jpackage.openjdk.java.net" in jpackagerTest + * keychain with always allowed access to this keychain for user which runs test. + */ + +/* + * @test + * @summary jpackage with --type pkg,dmg --mac-sign + * @library ../helpers + * @library /test/lib + * @library base + * @key jpackagePlatformPackage + * @build SigningBase + * @build SigningCheck + * @build jtreg.SkippedException + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @requires (os.family == "mac") + * @run main/othervm -Xmx512m SigningPackageTest + */ +public class SigningPackageTest { + + private static void verifyPKG(JPackageCommand cmd) { + Path outputBundle = cmd.outputBundle(); + SigningBase.verifyPkgutil(outputBundle); + SigningBase.verifySpctl(outputBundle, "install"); + } + + private static void verifyDMG(JPackageCommand cmd) { + Path outputBundle = cmd.outputBundle(); + SigningBase.verifyCodesign(outputBundle, false); + } + + private static void verifyAppImageInDMG(JPackageCommand cmd) { + MacHelper.withExplodedDmg(cmd, dmgImage -> { + Path launcherPath = dmgImage.resolve(Path.of("Contents", "MacOS", cmd.name())); + SigningBase.verifyCodesign(launcherPath, true); + SigningBase.verifyCodesign(dmgImage, true); + SigningBase.verifySpctl(dmgImage, "exec"); + }); + } + + public static void main(String[] args) throws Exception { + TKit.run(args, () -> { + SigningCheck.checkCertificates(); + + new PackageTest() + .configureHelloApp() + .forTypes(PackageType.MAC) + .addInitializer(cmd -> { + cmd.addArguments("--mac-sign", + "--mac-signing-key-user-name", SigningBase.DEV_NAME, + "--mac-signing-keychain", "jpackagerTest.keychain"); + }) + .forTypes(PackageType.MAC_PKG) + .addBundleVerifier(SigningPackageTest::verifyPKG) + .forTypes(PackageType.MAC_DMG) + .addBundleVerifier(SigningPackageTest::verifyDMG) + .addBundleVerifier(SigningPackageTest::verifyAppImageInDMG) + .run(); + }); + } +} diff --git a/test/jdk/tools/jpackage/macosx/base/SigningBase.java b/test/jdk/tools/jpackage/macosx/base/SigningBase.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/macosx/base/SigningBase.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import java.util.List; + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.Executor; + +public class SigningBase { + + public static String DEV_NAME = "jpackage.openjdk.java.net"; + public static String APP_CERT + = "Developer ID Application: " + DEV_NAME; + public static String INSTALLER_CERT + = "Developer ID Installer: " + DEV_NAME; + public static String KEYCHAIN = "jpackagerTest.keychain"; + + private static void checkString(List result, String lookupString) { + TKit.assertTextStream(lookupString).predicate( + (line, what) -> line.trim().equals(what)).apply(result.stream()); + } + + private static List codesignResult(Path target, boolean signed) { + int exitCode = signed ? 0 : 1; + List result = new Executor() + .setExecutable("codesign") + .addArguments("--verify", "--deep", "--strict", "--verbose=2", + target.toString()) + .saveOutput() + .execute() + .assertExitCodeIs(exitCode).getOutput(); + + return result; + } + + private static void verifyCodesignResult(List result, Path target, + boolean signed) { + result.stream().forEachOrdered(TKit::trace); + if (signed) { + String lookupString = target.toString() + ": valid on disk"; + checkString(result, lookupString); + lookupString = target.toString() + ": satisfies its Designated Requirement"; + checkString(result, lookupString); + } else { + String lookupString = target.toString() + + ": code object is not signed at all"; + checkString(result, lookupString); + } + } + + private static List spctlResult(Path target, String type) { + List result = new Executor() + .setExecutable("/usr/sbin/spctl") + .addArguments("-vvv", "--assess", "--type", type, + target.toString()) + .executeAndGetOutput(); + + return result; + } + + private static void verifySpctlResult(List result, Path target, String type) { + result.stream().forEachOrdered(TKit::trace); + String lookupString = target.toString() + ": accepted"; + checkString(result, lookupString); + lookupString = "source=" + DEV_NAME; + checkString(result, lookupString); + if (type.equals("install")) { + lookupString = "origin=" + INSTALLER_CERT; + } else { + lookupString = "origin=" + APP_CERT; + } + checkString(result, lookupString); + } + + private static List pkgutilResult(Path target) { + List result = new Executor() + .setExecutable("/usr/sbin/pkgutil") + .addArguments("--check-signature", + target.toString()) + .executeAndGetOutput(); + + return result; + } + + private static void verifyPkgutilResult(List result) { + result.stream().forEachOrdered(TKit::trace); + String lookupString = "Status: signed by a certificate trusted for current user"; + checkString(result, lookupString); + lookupString = "1. " + INSTALLER_CERT; + checkString(result, lookupString); + } + + public static void verifyCodesign(Path target, boolean signed) { + List result = codesignResult(target, signed); + verifyCodesignResult(result, target, signed); + } + + public static void verifySpctl(Path target, String type) { + List result = spctlResult(target, type); + verifySpctlResult(result, target, type); + } + + public static void verifyPkgutil(Path target) { + List result = pkgutilResult(target); + verifyPkgutilResult(result); + } + +} diff --git a/test/jdk/tools/jpackage/macosx/base/SigningCheck.java b/test/jdk/tools/jpackage/macosx/base/SigningCheck.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/macosx/base/SigningCheck.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.Executor; + +import jdk.incubator.jpackage.internal.MacCertificate; + +public class SigningCheck { + + public static void checkCertificates() { + List result = findCertificate(SigningBase.APP_CERT, SigningBase.KEYCHAIN); + String key = findKey(SigningBase.APP_CERT, result); + validateCertificate(key); + validateCertificateTrust(SigningBase.APP_CERT); + + result = findCertificate(SigningBase.INSTALLER_CERT, SigningBase.KEYCHAIN); + key = findKey(SigningBase.INSTALLER_CERT, result); + validateCertificate(key); + validateCertificateTrust(SigningBase.INSTALLER_CERT); + } + + private static List findCertificate(String name, String keyChain) { + List result = new Executor() + .setExecutable("security") + .addArguments("find-certificate", "-c", name, "-a", keyChain) + .executeAndGetOutput(); + + return result; + } + + private static String findKey(String name, List result) { + Pattern p = Pattern.compile("\"alis\"=\"([^\"]+)\""); + Matcher m = p.matcher(result.stream().collect(Collectors.joining())); + if (!m.find()) { + TKit.trace("Did not found a key for '" + name + "'"); + return null; + } + String matchedKey = m.group(1); + if (m.find()) { + TKit.trace("Found more than one key for '" + name + "'"); + return null; + } + TKit.trace("Using key '" + matchedKey); + return matchedKey; + } + + private static void validateCertificate(String key) { + if (key != null) { + MacCertificate certificate = new MacCertificate(key); + if (!certificate.isValid()) { + TKit.throwSkippedException("Certifcate expired: " + key); + } else { + return; + } + } + + TKit.throwSkippedException("Cannot find required certifciates: " + key); + } + + private static void validateCertificateTrust(String name) { + List result = new Executor() + .setExecutable("security") + .addArguments("dump-trust-settings") + .executeAndGetOutput(); + result.stream().forEachOrdered(TKit::trace); + TKit.assertTextStream(name) + .predicate((line, what) -> line.trim().endsWith(what)) + .orElseThrow(() -> TKit.throwSkippedException( + "Certifcate not trusted by current user: " + name)) + .apply(result.stream()); + } + +} diff --git a/test/jdk/tools/jpackage/manage_packages.sh b/test/jdk/tools/jpackage/manage_packages.sh new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/manage_packages.sh @@ -0,0 +1,231 @@ +#!/bin/bash + +# +# Script to install/uninstall packages produced by jpackage jtreg +# tests doing platform specific packaging. +# +# The script will install/uninstall all packages from the files +# found in the current directory or the one specified with command line option. +# +# When jtreg jpackage tests are executed with jpackage.test.output +# Java property set, produced package files (msi, exe, deb, rpm, etc.) will +# be saved in the directory specified with this property. +# +# Usage example: +# # Set directory where to save package files from jtreg jpackage tests +# JTREG_OUTPUT_DIR=/tmp/jpackage_jtreg_packages +# +# # Run tests and fill $JTREG_OUTPUT_DIR directory with package files +# jtreg -Djpackage.test.output=$JTREG_OUTPUT_DIR ... +# +# # Install all packages +# manage_pachages.sh -d $JTREG_OUTPUT_DIR +# +# # Uninstall all packages +# manage_pachages.sh -d $JTREG_OUTPUT_DIR -u +# + +# +# When using with MSI installers, Cygwin shell from which this script is +# executed should be started as administrator. Otherwise silent installation +# won't work. +# + +# Fail fast +set -e; set -o pipefail; + + +help_usage () +{ + echo "Usage: `basename $0` [OPTION]" + echo "Options:" + echo " -h - print this message" + echo " -v - verbose output" + echo " -d - path to directory where to look for package files" + echo " -u - uninstall packages instead of the default install" + echo " -t - dry run, print commands but don't execute them" +} + +error () +{ + echo "$@" > /dev/stderr +} + +fatal () +{ + error "$@" + exit 1 +} + +fatal_with_help_usage () +{ + error "$@" + help_usage + exit 1 +} + +# For macOS +if !(type "tac" &> /dev/null;) then + tac_cmd='tail -r' +else + tac_cmd=tac +fi + +# Directory where to look for package files. +package_dir=$PWD + +# Script debug. +verbose= + +# Operation mode. +mode=install + +dryrun= + +while getopts "vhd:ut" argname; do + case "$argname" in + v) verbose=yes;; + t) dryrun=yes;; + u) mode=uninstall;; + d) package_dir="$OPTARG";; + h) help_usage; exit 0;; + ?) help_usage; exit 1;; + esac +done +shift $(( OPTIND - 1 )) + +[ -d "$package_dir" ] || fatal_with_help_usage "Package directory [$package_dir] is not a directory" + +[ -z "$verbose" ] || set -x + + +function find_packages_of_type () +{ + # sort output alphabetically + find "$package_dir" -maxdepth 1 -type f -name '*.'"$1" | sort +} + +function find_packages () +{ + local package_suffixes=(deb rpm msi exe pkg dmg) + for suffix in "${package_suffixes[@]}"; do + if [ "$mode" == "uninstall" ]; then + packages=$(find_packages_of_type $suffix | $tac_cmd) + else + packages=$(find_packages_of_type $suffix) + fi + if [ -n "$packages" ]; then + package_type=$suffix + break; + fi + done +} + + +# RPM +install_cmd_rpm () +{ + echo sudo rpm --install "$@" +} +uninstall_cmd_rpm () +{ + local package_name=$(rpm -qp --queryformat '%{Name}' "$@") + echo sudo rpm -e "$package_name" +} + +# DEB +install_cmd_deb () +{ + echo sudo dpkg -i "$@" +} +uninstall_cmd_deb () +{ + local package_name=$(dpkg-deb -f "$@" Package) + echo sudo dpkg -r "$package_name" +} + +# MSI +install_cmd_msi () +{ + echo msiexec /qn /norestart /i $(cygpath -w "$@") +} +uninstall_cmd_msi () +{ + echo msiexec /qn /norestart /x $(cygpath -w "$@") +} + +# EXE +install_cmd_exe () +{ + echo "$@" +} +uninstall_cmd_exe () +{ + error No implemented +} + +# PKG +install_cmd_pkg () +{ + echo sudo /usr/sbin/installer -allowUntrusted -pkg "\"$@\"" -target / +} +uninstall_cmd_pkg () +{ + local pname=`basename $@` + local appname="$(cut -d'-' -f1 <<<"$pname")" + if [ "$appname" = "CommonInstallDirTest" ]; then + echo sudo rm -rf "/Applications/jpackage/\"$appname.app\"" + else + echo sudo rm -rf "/Applications/\"$appname.app\"" + fi +} + +# DMG +install_cmd_dmg () +{ + local pname=`basename $@` + local appname="$(cut -d'-' -f1 <<<"$pname")" + local command=() + if [ "$appname" = "CommonLicenseTest" ]; then + command+=("{" yes "|" hdiutil attach "\"$@\"" ">" /dev/null) + else + command+=("{" hdiutil attach "\"$@\"" ">" /dev/null) + fi + + command+=(";" sudo cp -R "\"/Volumes/$appname/$appname.app\"" /Applications ">" /dev/null) + command+=(";" hdiutil detach "\"/Volumes/$appname\"" ">" /dev/null ";}") + + echo "${command[@]}" +} +uninstall_cmd_dmg () +{ + local pname=`basename $@` + local appname="$(cut -d'-' -f1 <<<"$pname")" + echo sudo rm -rf "/Applications/\"$appname.app\"" +} + +# Find packages +packages= +find_packages +if [ -z "$packages" ]; then + echo "No packages found in $package_dir directory" + exit +fi + +# Build list of commands to execute +declare -a commands +IFS=$'\n' +for p in $packages; do + commands[${#commands[@]}]=$(${mode}_cmd_${package_type} "$p") +done + +if [ -z "$dryrun" ]; then + # Run commands + for cmd in "${commands[@]}"; do + echo Running: $cmd + eval $cmd || true; + done +else + # Print commands + for cmd in "${commands[@]}"; do echo $cmd; done +fi diff --git a/test/jdk/tools/jpackage/resources/icon.icns b/test/jdk/tools/jpackage/resources/icon.icns new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7e43c04df6a780d178172c282d6ec1798eea72d4 GIT binary patch literal 83256 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 M@&5-b4n{8jAO5tySpWb4 diff --git a/test/jdk/tools/jpackage/resources/icon.ico b/test/jdk/tools/jpackage/resources/icon.ico new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..00bc0ad44b7bac2426f5a110cf3ebbf185c358f3 GIT binary patch literal 2086 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 - path to JDK to be tested [ mandatory ]" + echo " -j - path to local copy of openjdk repo with jpackage jtreg tests" + echo " Optional, default is openjdk repo where this script resides" + echo " -o - path to folder where to copy artifacts for testing." + echo " Optional, default is the current directory." + echo ' -r - value for `jpackage.test.runtime-image` property.' + echo " Optional, for jtreg tests debug purposes only." + echo ' -l - value for `jpackage.test.logfile` property.' + echo " Optional, for jtreg tests debug purposes only." + echo " -m - mode to run jtreg tests." + echo ' Should be one of `create`, `update`, `verify-install` or `verify-uninstall`.' + echo ' Optional, default mode is `update`.' + echo ' - `create`' + echo ' Remove all package bundles from the output directory before running jtreg tests.' + echo ' - `update`' + echo ' Run jtreg tests and overrite existing package bundles in the output directory.' + echo ' - `verify-install`' + echo ' Verify installed packages created with the previous run of the script.' + echo ' - `verify-uninstall`' + echo ' Verify packages created with the previous run of the script were uninstalled cleanly.' + echo ' - `print-default-tests`' + echo ' Print default list of packaging tests and exit.' +} + +error () +{ + echo "$@" > /dev/stderr +} + +fatal () +{ + error "$@" + exit 1 +} + +fatal_with_help_usage () +{ + error "$@" + help_usage + exit 1 +} + +if command -v cygpath &> /dev/null; then +to_native_path () +{ + cygpath -m "$@" +} +else +to_native_path () +{ + echo "$@" +} +fi + +exec_command () +{ + if [ -n "$dry_run" ]; then + echo "$@" + else + eval "$@" + fi +} + + +# Path to JDK to be tested. +test_jdk= + +# Path to local copy of open jdk repo with jpackage jtreg tests +# hg clone http://hg.openjdk.java.net/jdk/sandbox +# cd sandbox; hg update -r JDK-8200758-branch +open_jdk_with_jpackage_jtreg_tests=$(dirname $0)/../../../../ + +# Directory where to save artifacts for testing. +output_dir=$PWD + +# Script and jtreg debug. +verbose= +jtreg_verbose="-verbose:fail,error,summary" + +keep_jtreg_cache= + +# Mode in which to run jtreg tests +mode=update + +# jtreg extra arguments +declare -a jtreg_args + +# Run all tests +run_all_tests= + +mapfile -t tests < <(find_all_packaging_tests) + +while getopts "vahdct:j:o:r:m:l:" argname; do + case "$argname" in + v) verbose=yes;; + a) run_all_tests=yes;; + d) dry_run=yes;; + c) keep_jtreg_cache=yes;; + t) test_jdk="$OPTARG";; + j) open_jdk_with_jpackage_jtreg_tests="$OPTARG";; + o) output_dir="$OPTARG";; + r) runtime_dir="$OPTARG";; + l) logfile="$OPTARG";; + m) mode="$OPTARG";; + h) help_usage; exit 0;; + ?) help_usage; exit 1;; + esac +done +shift $(( OPTIND - 1 )) + +[ -z "$verbose" ] || { set -x; jtreg_verbose=-va; } + +if [ -z "$open_jdk_with_jpackage_jtreg_tests" ]; then + fatal_with_help_usage "Path to openjdk repo with jpackage jtreg tests not specified" +fi + +if [ "$mode" = "print-default-tests" ]; then + exec_command for t in ${tests[@]}";" do echo '$t;' done + exit +fi + +if [ -z "$test_jdk" ]; then + fatal_with_help_usage Path to test JDK not specified +fi + +if [ -z "$JAVA_HOME" ]; then + echo JAVA_HOME environment variable not set, will use java from test JDK [$test_jdk] to run jtreg + JAVA_HOME="$test_jdk" +fi +if [ ! -e "$JAVA_HOME/bin/java" ]; then + fatal JAVA_HOME variable is set to [$JAVA_HOME] value, but $JAVA_HOME/bin/java not found. +fi + +if [ -n "$runtime_dir" ]; then + if [ ! -d "$runtime_dir" ]; then + fatal 'Value of `-r` option is set to non-existing directory'. + fi + jtreg_args+=("-Djpackage.test.runtime-image=$(to_native_path "$(cd "$runtime_dir" && pwd)")") +fi + +if [ -n "$logfile" ]; then + if [ ! -d "$(dirname "$logfile")" ]; then + fatal 'Value of `-l` option specified a file in non-existing directory'. + fi + logfile="$(cd "$(dirname "$logfile")" && pwd)/$(basename "$logfile")" + jtreg_args+=("-Djpackage.test.logfile=$(to_native_path "$logfile")") +fi + +if [ "$mode" = create ]; then + true +elif [ "$mode" = update ]; then + true +elif [ "$mode" = verify-install ]; then + jtreg_args+=("-Djpackage.test.action=$mode") +elif [ "$mode" = verify-uninstall ]; then + jtreg_args+=("-Djpackage.test.action=$mode") +else + fatal_with_help_usage 'Invalid value of -m option:' [$mode] +fi + +if [ -z "$run_all_tests" ]; then + jtreg_args+=(-Djpackage.test.SQETest=yes) +fi + +# All remaining command line arguments are tests to run that should override the defaults +[ $# -eq 0 ] || tests=($@) + + +installJtreg () +{ + # Install jtreg if missing + if [ ! -f "$jtreg_jar" ]; then + exec_command mkdir -p "$workdir" + exec_command "(" cd "$workdir" "&&" wget "$jtreg_bundle" "&&" tar -xzf "$(basename $jtreg_bundle)" ";" rm -f "$(basename $jtreg_bundle)" ")" + fi +} + + +preRun () +{ + local xargs_args=(-t --no-run-if-empty rm) + if [ -n "$dry_run" ]; then + xargs_args=(--no-run-if-empty echo rm) + fi + + if [ ! -d "$output_dir" ]; then + exec_command mkdir -p "$output_dir" + fi + [ ! -d "$output_dir" ] || output_dir=$(cd "$output_dir" && pwd) + + # Clean output directory + [ "$mode" != "create" ] || find $output_dir -maxdepth 1 -type f -name '*.exe' -or -name '*.msi' -or -name '*.rpm' -or -name '*.deb' | xargs "${xargs_args[@]}" +} + + +run () +{ + local jtreg_cmdline=(\ + $JAVA_HOME/bin/java -jar $(to_native_path "$jtreg_jar") \ + "-Djpackage.test.output=$(to_native_path "$output_dir")" \ + "${jtreg_args[@]}" \ + -nr \ + "$jtreg_verbose" \ + -retain:all \ + -automatic \ + -ignore:run \ + -testjdk:"$(to_native_path $test_jdk)" \ + -dir:"$(to_native_path $open_jdk_with_jpackage_jtreg_tests)" \ + -reportDir:"$(to_native_path $workdir/run/results)" \ + -workDir:"$(to_native_path $workdir/run/support)" \ + "${tests[@]}" \ + ) + + # Clear previous results + [ -n "$keep_jtreg_cache" ] || exec_command rm -rf "$workdir"/run + + # Run jpackage jtreg tests to create artifacts for testing + exec_command ${jtreg_cmdline[@]} +} + + +installJtreg +preRun +run diff --git a/test/jdk/tools/jpackage/share/AddLauncherBase.java b/test/jdk/tools/jpackage/share/AddLauncherBase.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/AddLauncherBase.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +public class AddLauncherBase { + private static final String app = JPackagePath.getApp(); + private static final String appOutput = JPackagePath.getAppOutputFile(); + + // Note: quotes in argument for add launcher is not support by test + private static final String ARGUMENT1 = "argument 1"; + private static final String ARGUMENT2 = "argument 2"; + private static final String ARGUMENT3 = "argument 3"; + + private static final List arguments = new ArrayList<>(); + + private static final String PARAM1 = "-Dparam1=Some Param 1"; + private static final String PARAM2 = "-Dparam2=Some Param 2"; + private static final String PARAM3 = "-Dparam3=Some Param 3"; + + private static final List vmArguments = new ArrayList<>(); + private static final List empty = new ArrayList<>(); + + private static void validateResult(List args, List vmArgs) + throws Exception { + File outfile = new File(appOutput); + if (!outfile.exists()) { + throw new AssertionError(appOutput + " was not created"); + } + + String output = Files.readString(outfile.toPath()); + String[] result = output.split("\n"); + + int expected = 2 + args.size() + vmArgs.size(); + + if (result.length != expected) { + throw new AssertionError("Unexpected number of lines: " + + result.length + " expected: " + expected + " - results: " + output); + } + + if (!result[0].trim().endsWith("jpackage test application")) { + throw new AssertionError("Unexpected result[0]: " + result[0]); + } + + if (!result[1].trim().equals("args.length: " + args.size())) { + throw new AssertionError("Unexpected result[1]: " + result[1]); + } + + int index = 2; + for (String arg : args) { + if (!result[index].trim().equals(arg)) { + throw new AssertionError("Unexpected result[" + + index + "]: " + result[index]); + } + index++; + } + + for (String vmArg : vmArgs) { + if (!result[index].trim().equals(vmArg)) { + throw new AssertionError("Unexpected result[" + + index + "]: " + result[index]); + } + index++; + } + } + + private static void validate(boolean includeArgs, String name) + throws Exception { + int retVal = JPackageHelper.execute(null, app); + if (retVal != 0) { + throw new AssertionError("Test application " + app + + " exited with error: " + retVal); + } + validateResult(new ArrayList<>(), new ArrayList<>()); + + String app2 = JPackagePath.getAppSL(name); + retVal = JPackageHelper.execute(null, app2); + if (retVal != 0) { + throw new AssertionError("Test application " + app2 + + " exited with error: " + retVal); + } + if (includeArgs) { + validateResult(arguments, vmArguments); + } else { + validateResult(empty, empty); + } + } + + public static void testCreateAppImage(String [] cmd) throws Exception { + testCreateAppImage(cmd, true, "test2"); + } + + public static void testCreateAppImage(String [] cmd, + boolean includeArgs, String name) throws Exception { + JPackageHelper.executeCLI(true, cmd); + validate(includeArgs, name); + } + + public static void testCreateAppImageToolProvider(String [] cmd) + throws Exception { + testCreateAppImageToolProvider(cmd, true, "test2"); + } + + public static void testCreateAppImageToolProvider(String [] cmd, + boolean includeArgs, String name) throws Exception { + JPackageHelper.executeToolProvider(true, cmd); + validate(includeArgs, name); + } + + public static void testCreateAppImage(String [] cmd, + ArrayList argList, ArrayList optionList) + throws Exception { + JPackageHelper.executeCLI(true, cmd); + int retVal = JPackageHelper.execute(null, app); + if (retVal != 0) { + throw new AssertionError("Test application " + app + + " exited with error: " + retVal); + } + validateResult(argList, optionList); + String name = "test4"; + + String app2 = JPackagePath.getAppSL(name); + retVal = JPackageHelper.execute(null, app2); + if (retVal != 0) { + throw new AssertionError("Test application " + app2 + + " exited with error: " + retVal); + } + validateResult(arguments, vmArguments); + } + + public static void createSLProperties() throws Exception { + arguments.add(ARGUMENT1); + arguments.add(ARGUMENT2); + arguments.add(ARGUMENT3); + + String argumentsMap = + JPackageHelper.listToArgumentsMap(arguments, true); + + vmArguments.add(PARAM1); + vmArguments.add(PARAM2); + vmArguments.add(PARAM3); + + String vmArgumentsMap = + JPackageHelper.listToArgumentsMap(vmArguments, true); + + try (PrintWriter out = new PrintWriter(new BufferedWriter( + new FileWriter("sl.properties")))) { + out.println("arguments=" + argumentsMap); + out.println("java-options=" + vmArgumentsMap); + } + + try (PrintWriter out = new PrintWriter(new BufferedWriter( + new FileWriter("m1.properties")))) { + out.println("module=com.hello/com.hello.Hello"); + out.println("main-jar="); + } + + try (PrintWriter out = new PrintWriter(new BufferedWriter( + new FileWriter("j1.properties")))) { + out.println("main-jar hello.jar"); + out.println("main-class Hello"); + } + + + } + +} diff --git a/test/jdk/tools/jpackage/share/AddLauncherModuleTest.java b/test/jdk/tools/jpackage/share/AddLauncherModuleTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/AddLauncherModuleTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + /* + * @test + * @summary jpackage create image with additional launcher test + * @library ../helpers + * @build JPackageHelper + * @build JPackagePath + * @build AddLauncherBase + * @modules jdk.incubator.jpackage + * @run main/othervm -Xmx512m AddLauncherModuleTest + */ +public class AddLauncherModuleTest { + private static final String OUTPUT = "output"; + private static final String [] CMD = { + "--type", "app-image", + "--dest", OUTPUT, + "--name", "test", + "--module", "com.hello/com.hello.Hello", + "--module-path", "input", + "--add-launcher", "test2=sl.properties"}; + + public static void main(String[] args) throws Exception { + JPackageHelper.createHelloModule(); + AddLauncherBase.createSLProperties(); + AddLauncherBase.testCreateAppImageToolProvider( + CMD); + } + +} diff --git a/test/jdk/tools/jpackage/share/AddLauncherTest.java b/test/jdk/tools/jpackage/share/AddLauncherTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/AddLauncherTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.util.ArrayList; + +/* + * @test + * @summary jpackage create image with additional launcher test + * @library ../helpers + * @build JPackageHelper + * @build JPackagePath + * @build AddLauncherBase + * @modules jdk.incubator.jpackage + * @run main/othervm -Xmx512m AddLauncherTest + */ +public class AddLauncherTest { + private static final String OUTPUT = "output"; + private static final String [] CMD = { + "--type", "app-image", + "--input", "input", + "--dest", OUTPUT, + "--name", "test", + "--main-jar", "hello.jar", + "--main-class", "Hello", + "--add-launcher", "test2=sl.properties"}; + + private final static String OPT1 = "-Dparam1=xxx"; + private final static String OPT2 = "-Dparam2=yyy"; + private final static String OPT3 = "-Dparam3=zzz"; + private final static String ARG1 = "original-argument"; + + private static final String [] CMD1 = { + "--type", "app-image", + "--input", "input", + "--dest", OUTPUT, + "--name", "test", + "--main-jar", "hello.jar", + "--main-class", "Hello", + "--java-options", OPT1, + "--java-options", OPT2, + "--java-options", OPT3, + "--arguments", ARG1, + "--add-launcher", "test4=sl.properties"}; + + + public static void main(String[] args) throws Exception { + JPackageHelper.createHelloImageJar(); + AddLauncherBase.createSLProperties(); + AddLauncherBase.testCreateAppImage(CMD); + + ArrayList argList = new ArrayList (); + argList.add(ARG1); + + ArrayList optList = new ArrayList (); + optList.add(OPT1); + optList.add(OPT2); + optList.add(OPT3); + + JPackageHelper.deleteOutputFolder(OUTPUT); + AddLauncherBase.testCreateAppImage(CMD1, argList, optList); + } + +} diff --git a/test/jdk/tools/jpackage/share/AddLaunchersTest.java b/test/jdk/tools/jpackage/share/AddLaunchersTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/AddLaunchersTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + /* + * @test + * @summary jpackage create image with additional launcher test + * @library ../helpers + * @build JPackageHelper + * @build JPackagePath + * @build AddLauncherBase + * @modules jdk.incubator.jpackage + * @run main/othervm -Xmx512m AddLaunchersTest + */ +public class AddLaunchersTest { + private static final String OUTPUT = "output"; + private static final String [] CMD1 = { + "--description", "Test non modular app with multiple add-launchers where one is modular app and other is non modular app", + "--type", "app-image", + "--input", "input", + "--dest", OUTPUT, + "--name", "test", + "--main-jar", "hello.jar", + "--main-class", "Hello", + "--module-path", "module", + "--add-modules", "com.hello,java.desktop", + "--add-launcher", "test3=j1.properties", + "--add-launcher", "test4=m1.properties"}; + + private static final String [] CMD2 = { + "--description", "Test modular app with multiple add-launchers where one is modular app and other is non modular app", + "--type", "app-image", + "--input", "input", + "--dest", OUTPUT, + "--name", "test", + "--module", "com.hello/com.hello.Hello", + "--module-path", "module", + "--add-launcher", "test5=jl.properties", + "--add-launcher", "test6=m1.properties"}; + + public static void main(String[] args) throws Exception { + JPackageHelper.createHelloImageJar(); + JPackageHelper.createHelloModule(); + AddLauncherBase.createSLProperties(); + + JPackageHelper.deleteOutputFolder(OUTPUT); + AddLauncherBase.testCreateAppImageToolProvider( + CMD1, false, "test3"); + + JPackageHelper.deleteOutputFolder(OUTPUT); + AddLauncherBase.testCreateAppImage( + CMD1, false, "test4"); + + JPackageHelper.deleteOutputFolder(OUTPUT); + AddLauncherBase.testCreateAppImage( + CMD2, false, "test5"); + + JPackageHelper.deleteOutputFolder(OUTPUT); + AddLauncherBase.testCreateAppImageToolProvider( + CMD2, false, "test6"); + + } + +} diff --git a/test/jdk/tools/jpackage/share/AdditionalLaunchersTest.java b/test/jdk/tools/jpackage/share/AdditionalLaunchersTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/AdditionalLaunchersTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.Optional; +import java.lang.invoke.MethodHandles; +import jdk.jpackage.test.HelloApp; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.FileAssociations; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.TKit; + +/** + * Test --add-launcher parameter. Output of the test should be + * additionallauncherstest*.* installer. The output installer should provide the + * same functionality as the default installer (see description of the default + * installer in SimplePackageTest.java) plus install three extra application + * launchers. + */ + +/* + * @test + * @summary jpackage with --add-launcher + * @key jpackagePlatformPackage + * @library ../helpers + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile AdditionalLaunchersTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=AdditionalLaunchersTest + */ + +public class AdditionalLaunchersTest { + + @Test + public void test() { + // Configure a bunch of additional launchers and also setup + // file association to make sure it will be linked only to the main + // launcher. + + PackageTest packageTest = new PackageTest().configureHelloApp(); + packageTest.addInitializer(cmd -> { + cmd.addArguments("--arguments", "Duke", "--arguments", "is", + "--arguments", "the", "--arguments", "King"); + }); + + new FileAssociations( + MethodHandles.lookup().lookupClass().getSimpleName()).applyTo( + packageTest); + + new AdditionalLauncher("Baz2").setArguments().applyTo(packageTest); + new AdditionalLauncher("foo").setArguments("yep!").applyTo(packageTest); + + AdditionalLauncher barLauncher = new AdditionalLauncher("Bar").setArguments( + "one", "two", "three"); + if (TKit.isLinux()) { + barLauncher.setIcon(TKit.TEST_SRC_ROOT.resolve("apps/dukeplug.png")); + } + barLauncher.applyTo(packageTest); + + packageTest.run(); + } + + private static Path replaceFileName(Path path, String newFileName) { + String fname = path.getFileName().toString(); + int lastDotIndex = fname.lastIndexOf("."); + if (lastDotIndex != -1) { + fname = newFileName + fname.substring(lastDotIndex); + } else { + fname = newFileName; + } + return path.getParent().resolve(fname); + } + + static class AdditionalLauncher { + + AdditionalLauncher(String name) { + this.name = name; + } + + AdditionalLauncher setArguments(String... args) { + arguments = List.of(args); + return this; + } + + AdditionalLauncher setIcon(Path iconPath) { + icon = iconPath; + return this; + } + + void applyTo(PackageTest test) { + final Path propsFile = TKit.workDir().resolve(name + ".properties"); + + test.addInitializer(cmd -> { + cmd.addArguments("--add-launcher", String.format("%s=%s", name, + propsFile)); + + Map properties = new HashMap<>(); + if (arguments != null) { + properties.put("arguments", String.join(" ", + arguments.toArray(String[]::new))); + } + + if (icon != null) { + properties.put("icon", icon.toAbsolutePath().toString()); + } + + TKit.createPropertiesFile(propsFile, properties); + }); + test.addInstallVerifier(cmd -> { + Path launcherPath = replaceFileName(cmd.appLauncherPath(), name); + + TKit.assertExecutableFileExists(launcherPath); + + if (cmd.isFakeRuntime(String.format( + "Not running %s launcher", launcherPath))) { + return; + } + HelloApp.executeAndVerifyOutput(launcherPath, + Optional.ofNullable(arguments).orElse(List.of()).toArray( + String[]::new)); + }); + test.addUninstallVerifier(cmd -> { + Path launcherPath = replaceFileName(cmd.appLauncherPath(), name); + + TKit.assertPathExists(launcherPath, false); + }); + } + + private List arguments; + private Path icon; + private final String name; + } +} diff --git a/test/jdk/tools/jpackage/share/AppImagePackageTest.java b/test/jdk/tools/jpackage/share/AppImagePackageTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/AppImagePackageTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.JPackageCommand; + +/** + * Test --app-image parameter. The output installer should provide the same + * functionality as the default installer (see description of the default + * installer in SimplePackageTest.java) + */ + +/* + * @test + * @summary jpackage with --app-image + * @key jpackagePlatformPackage + * @library ../helpers + * @requires (jpackage.test.SQETest == null) + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @run main/othervm/timeout=540 -Xmx512m AppImagePackageTest + */ +public class AppImagePackageTest { + + public static void main(String[] args) { + TKit.run(args, () -> { + Path appimageOutput = Path.of("appimage"); + + JPackageCommand appImageCmd = JPackageCommand.helloAppImage() + .setArgumentValue("--dest", appimageOutput) + .addArguments("--type", "app-image"); + + PackageTest packageTest = new PackageTest(); + if (packageTest.getAction() == PackageTest.Action.CREATE) { + appImageCmd.execute(); + } + + packageTest.addInitializer(cmd -> { + Path appimageInput = appimageOutput.resolve(appImageCmd.name()); + + if (PackageType.MAC.contains(cmd.packageType())) { + // Why so complicated on macOS? + appimageInput = Path.of(appimageInput.toString() + ".app"); + } + + cmd.addArguments("--app-image", appimageInput); + cmd.removeArgumentWithValue("--input"); + }).addBundleDesktopIntegrationVerifier(false).run(); + }); + } +} diff --git a/test/jdk/tools/jpackage/share/ArgumentsTest.java b/test/jdk/tools/jpackage/share/ArgumentsTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/ArgumentsTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import java.util.List; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.HelloApp; +import jdk.jpackage.test.Functional.ThrowingConsumer; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.Annotations.*; + + +/* + * Tricky arguments used in the test require a bunch of levels of character + * escaping for proper encoding them in a single string to be used as a value of + * `--arguments` option. String with encoded arguments doesn't go through the + * system to jpackage executable as is because OS is interpreting escape + * characters. This is true for Windows at least. + * + * String mapping performed by the system corrupts the string and jpackage exits + * with error. There is no problem with string corruption when jpackage is used + * as tool provider. This is not jpackage issue, so just always run this test + * with jpackage used as tool provider. + * / + +/* + * @test + * @summary jpackage create image with --arguments test + * @library ../helpers + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile ArgumentsTest.java + * @run main/othervm -Xmx512m jdk.jpackage.test.Main + * --jpt-run=ArgumentsTest + */ +public class ArgumentsTest { + + @BeforeEach + public static void useJPackageToolProvider() { + JPackageCommand.useToolProviderByDefault(); + } + + @Test + @Parameter("Goodbye") + @Parameter("com.hello/com.hello.Hello") + public static void testApp(String javaAppDesc) { + testIt(javaAppDesc, null); + } + + private static void testIt(String javaAppDesc, + ThrowingConsumer initializer) { + + JPackageCommand cmd = JPackageCommand.helloAppImage(javaAppDesc).addArguments( + "--arguments", JPackageCommand.escapeAndJoin(TRICKY_ARGUMENTS)); + if (initializer != null) { + ThrowingConsumer.toConsumer(initializer).accept(cmd); + } + + cmd.executeAndAssertImageCreated(); + + Path launcherPath = cmd.appLauncherPath(); + if (!cmd.isFakeRuntime(String.format( + "Not running [%s] launcher", launcherPath))) { + HelloApp.executeAndVerifyOutput(launcherPath, TRICKY_ARGUMENTS); + } + } + + private final static List TRICKY_ARGUMENTS = List.of( + "argument", + "Some Arguments", + "Value \"with\" quotes" + ); +} diff --git a/test/jdk/tools/jpackage/share/Base.java b/test/jdk/tools/jpackage/share/Base.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/Base.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Files; +import java.nio.file.Path; + +public abstract class Base { + private static final String appOutput = JPackagePath.getAppOutputFile(); + + private static void validateResult(String[] result) throws Exception { + if (result.length != 2) { + throw new AssertionError( + "Unexpected number of lines: " + result.length); + } + + if (!result[0].trim().endsWith("jpackage test application")) { + throw new AssertionError("Unexpected result[0]: " + result[0]); + } + + if (!result[1].trim().equals("args.length: 0")) { + throw new AssertionError("Unexpected result[1]: " + result[1]); + } + } + + public static void validate(String app) throws Exception { + Path outPath = Path.of(appOutput); + int retVal = JPackageHelper.execute(null, app); + + if (outPath.toFile().exists()) { + System.out.println("output contents: "); + System.out.println(Files.readString(outPath) + "\n"); + } else { + System.out.println("no output file: " + outPath + + " from command: " + app); + } + + if (retVal != 0) { + throw new AssertionError( + "Test application (" + app + ") exited with error: " + retVal); + } + + if (!outPath.toFile().exists()) { + throw new AssertionError(appOutput + " was not created"); + } + + String output = Files.readString(outPath); + String[] result = JPackageHelper.splitAndFilter(output); + validateResult(result); + } + + public static void testCreateAppImage(String [] cmd) throws Exception { + JPackageHelper.executeCLI(true, cmd); + validate(JPackagePath.getApp()); + } + + public static void testCreateAppImageToolProvider(String [] cmd) throws Exception { + JPackageHelper.executeToolProvider(true, cmd); + validate(JPackagePath.getApp()); + } +} diff --git a/test/jdk/tools/jpackage/share/ErrorTest.java b/test/jdk/tools/jpackage/share/ErrorTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/ErrorTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + /* + * @test + * @summary jpackage create app image error test + * @library ../helpers + * @build JPackageHelper + * @build JPackagePath + * @build Base + * @modules jdk.incubator.jpackage + * @run main/othervm -Xmx512m ErrorTest + */ +import java.util.*; +import java.io.*; +import java.nio.*; +import java.nio.file.*; +import java.nio.file.attribute.*; + +public class ErrorTest { + + private static final String OUTPUT = "output"; + + private static final String ARG1 = "--no-such-argument"; + private static final String EXPECTED1 = + "Invalid Option: [--no-such-argument]"; + private static final String ARG2 = "--dest"; + private static final String EXPECTED2 = "--main-jar or --module"; + + private static final String [] CMD1 = { + "--type", "app-image", + "--input", "input", + "--dest", OUTPUT, + "--name", "test", + "--main-jar", "non-existant.jar", + }; + private static final String EXP1 = "main jar does not exist"; + + private static final String [] CMD2 = { + "--type", "app-image", + "--input", "input", + "--dest", OUTPUT, + "--name", "test", + "--main-jar", "hello.jar", + }; + private static final String EXP2 = "class was not specified nor was"; + + private static void validate(String output, String expected, boolean single) + throws Exception { + String[] result = JPackageHelper.splitAndFilter(output); + if (single && result.length != 1) { + System.err.println(output); + throw new AssertionError("Unexpected multiple lines of output: " + + output); + } + + if (!result[0].trim().contains(expected)) { + throw new AssertionError("Unexpected output: " + result[0] + + " - expected output to contain: " + expected); + } + } + + + public static void main(String[] args) throws Exception { + JPackageHelper.createHelloImageJar(); + + validate(JPackageHelper.executeToolProvider(false, + "--type", "app-image", ARG1), EXPECTED1, true); + validate(JPackageHelper.executeToolProvider(false, + "--type", "app-image", ARG2), EXPECTED2, true); + + JPackageHelper.deleteOutputFolder(OUTPUT); + validate(JPackageHelper.executeToolProvider(false, CMD1), EXP1, false); + + JPackageHelper.deleteOutputFolder(OUTPUT); + validate(JPackageHelper.executeToolProvider(false, CMD2), EXP2, false); + + } + +} diff --git a/test/jdk/tools/jpackage/share/FileAssociationsTest.java b/test/jdk/tools/jpackage/share/FileAssociationsTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/FileAssociationsTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.FileAssociations; +import jdk.jpackage.test.Annotations.Test; + +/** + * Test --file-associations parameter. Output of the test should be + * fileassociationstest*.* installer. The output installer should provide the + * same functionality as the default installer (see description of the default + * installer in SimplePackageTest.java) plus configure file associations. After + * installation files with ".jptest1" and ".jptest2" suffixes should be + * associated with the test app. + * + * Suggested test scenario is to create empty file with ".jptest1" suffix, + * double click on it and make sure that test application was launched in + * response to double click event with the path to test .jptest1 file on the + * commend line. The same applies to ".jptest2" suffix. + * + * On Linux use "echo > foo.jptest1" and not "touch foo.jptest1" to create test + * file as empty files are always interpreted as plain text and will not be + * opened with the test app. This is a known bug. + * + * Icon associated with the main launcher should be associated with files with + * ".jptest1" suffix. Different icon should be associated with files with with + * ".jptest2" suffix. Icon for files with ".jptest1" suffix is platform specific + * and is one of 'icon.*' files in test/jdk/tools/jpackage/resources directory. + */ + +/* + * @test + * @summary jpackage with --file-associations + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile FileAssociationsTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=FileAssociationsTest + */ +public class FileAssociationsTest { + @Test + public static void test() { + PackageTest packageTest = new PackageTest(); + + // Not supported + packageTest.excludeTypes(PackageType.MAC_DMG); + + new FileAssociations("jptest1").applyTo(packageTest); + + Path icon = TKit.TEST_SRC_ROOT.resolve(Path.of("resources", "icon" + + TKit.ICON_SUFFIX)); + + icon = TKit.createRelativePathCopy(icon); + + new FileAssociations("jptest2") + .setFilename("fa2") + .setIcon(icon) + .applyTo(packageTest); + + packageTest.run(); + } +} diff --git a/test/jdk/tools/jpackage/share/IconTest.java b/test/jdk/tools/jpackage/share/IconTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/IconTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import jdk.incubator.jpackage.internal.IOUtils; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.Functional; +import jdk.jpackage.test.Annotations.*; +import jdk.jpackage.test.JPackageCommand; + +/* + * @test + * @summary jpackage create image with custom icon + * @library ../helpers + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile IconTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=IconTest + */ + +public class IconTest { + @Test + public static void testResourceDir() throws IOException { + TKit.withTempDirectory("resources", tempDir -> { + JPackageCommand cmd = JPackageCommand.helloAppImage() + .addArguments("--resource-dir", tempDir); + + Files.copy(GOLDEN_ICON, tempDir.resolve(appIconFileName(cmd)), + StandardCopyOption.REPLACE_EXISTING); + + testIt(cmd); + }); + } + + @Test + @Parameter("true") + @Parameter("false") + public static void testParameter(boolean relativePath) throws IOException { + final Path iconPath; + if (relativePath) { + iconPath = TKit.createRelativePathCopy(GOLDEN_ICON); + } else { + iconPath = GOLDEN_ICON; + } + + testIt(JPackageCommand.helloAppImage().addArguments("--icon", iconPath)); + } + + private static String appIconFileName(JPackageCommand cmd) { + return IOUtils.replaceSuffix(cmd.appLauncherPath().getFileName(), + TKit.ICON_SUFFIX).toString(); + } + + private static void testIt(JPackageCommand cmd) throws IOException { + cmd.executeAndAssertHelloAppImageCreated(); + + Path iconPath = cmd.appLayout().destktopIntegrationDirectory().resolve( + appIconFileName(cmd)); + + TKit.assertFileExists(iconPath); + TKit.assertTrue(-1 == Files.mismatch(GOLDEN_ICON, iconPath), + String.format( + "Check application icon file [%s] is a copy of source icon file [%s]", + iconPath, GOLDEN_ICON)); + } + + private final static Path GOLDEN_ICON = TKit.TEST_SRC_ROOT.resolve(Path.of( + "resources", "icon" + TKit.ICON_SUFFIX)); +} diff --git a/test/jdk/tools/jpackage/share/InstallDirTest.java b/test/jdk/tools/jpackage/share/InstallDirTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/InstallDirTest.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.Functional; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.Annotations.Parameter; + +/** + * Test --install-dir parameter. Output of the test should be + * commoninstalldirtest*.* package bundle. The output package should provide the + * same functionality as the default package but install test application in + * specified directory. + * + * Linux: + * + * Application should be installed in /opt/jpackage/commoninstalldirtest folder. + * + * Mac: + * + * Application should be installed in /Applications/jpackage/commoninstalldirtest.app + * folder. + * + * Windows: + * + * Application should be installed in %ProgramFiles%/TestVendor/InstallDirTest1234 + * folder. + */ + +/* + * @test + * @summary jpackage with --install-dir + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @compile InstallDirTest.java + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @run main/othervm/timeout=540 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=InstallDirTest.testCommon + */ + +/* + * @test + * @summary jpackage with --install-dir + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @compile InstallDirTest.java + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @requires (os.family == "linux") + * @requires (jpackage.test.SQETest == null) + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=InstallDirTest.testLinuxInvalid,testLinuxUnsupported + */ +public class InstallDirTest { + + public static void testCommon() { + final Map INSTALL_DIRS = Functional.identity(() -> { + Map reply = new HashMap<>(); + reply.put(PackageType.WIN_MSI, Path.of("TestVendor\\InstallDirTest1234")); + reply.put(PackageType.WIN_EXE, reply.get(PackageType.WIN_MSI)); + + reply.put(PackageType.LINUX_DEB, Path.of("/opt/jpackage")); + reply.put(PackageType.LINUX_RPM, reply.get(PackageType.LINUX_DEB)); + + reply.put(PackageType.MAC_PKG, Path.of("/Applications/jpackage")); + + return reply; + }).get(); + + new PackageTest().excludeTypes(PackageType.MAC_DMG).configureHelloApp() + .addInitializer(cmd -> { + cmd.addArguments("--install-dir", INSTALL_DIRS.get( + cmd.packageType())); + }).run(); + } + + @Parameter("/") + @Parameter(".") + @Parameter("foo") + @Parameter("/opt/foo/.././.") + public static void testLinuxInvalid(String installDir) { + testLinuxBad(installDir, "Invalid installation directory"); + } + + @Parameter("/usr") + @Parameter("/usr/local") + @Parameter("/usr/foo") + public static void testLinuxUnsupported(String installDir) { + testLinuxBad(installDir, "currently unsupported"); + } + + private static void testLinuxBad(String installDir, + String errorMessageSubstring) { + new PackageTest().configureHelloApp() + .setExpectedExitCode(1) + .forTypes(PackageType.LINUX) + .addInitializer(cmd -> { + cmd.addArguments("--install-dir", installDir); + cmd.saveConsoleOutput(true); + }) + .addBundleVerifier((cmd, result) -> { + String errorMessage = JPackageCommand.filterOutput( + result.getOutput().stream()).filter(line -> line.contains( + errorMessageSubstring)).findFirst().orElse(null); + TKit.assertNotNull(errorMessage, String.format( + "Check output contains [%s] substring", + errorMessageSubstring)); + }) + .run(); + } +} diff --git a/test/jdk/tools/jpackage/share/InvalidArgTest.java b/test/jdk/tools/jpackage/share/InvalidArgTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/InvalidArgTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + /* + * @test + * @summary jpackage invalid argument test + * @library ../helpers + * @build JPackageHelper + * @build JPackagePath + * @modules jdk.incubator.jpackage + * @run main/othervm -Xmx512m InvalidArgTest + */ +public class InvalidArgTest { + + private static final String ARG1 = "--no-such-argument"; + private static final String ARG2 = "--dest"; + private static final String RESULT1 = + "Invalid Option: [--no-such-argument]"; + private static final String RESULT2 = "--main-jar or --module"; + + private static void validate(String arg, String output) throws Exception { + String[] result = JPackageHelper.splitAndFilter(output); + if (result.length != 1) { + System.err.println(output); + throw new AssertionError("Invalid number of lines in output: " + + result.length); + } + + if (arg.equals(ARG1)) { + if (!result[0].trim().contains(RESULT1)) { + System.err.println("Expected: " + RESULT1); + System.err.println("Actual: " + result[0]); + throw new AssertionError("Unexpected output: " + result[0]); + } + } else if (arg.equals(ARG2)) { + if (!result[0].trim().contains(RESULT2)) { + System.err.println("Expected: " + RESULT2); + System.err.println("Actual: " + result[0]); + throw new AssertionError("Unexpected output: " + result[0]); + } + } + } + + private static void testInvalidArg() throws Exception { + String output = JPackageHelper.executeCLI(false, + "--type", "app-image", ARG1); + validate(ARG1, output); + + output = JPackageHelper.executeCLI(false, + "--type", "app-image", ARG2); + validate(ARG2, output); + } + + private static void testInvalidArgToolProvider() throws Exception { + String output = JPackageHelper.executeToolProvider(false, + "--type", "app-image", ARG1); + validate(ARG1, output); + + output = JPackageHelper.executeToolProvider(false, + "--type", "app-image", ARG2); + validate(ARG2, output); + } + + public static void main(String[] args) throws Exception { + testInvalidArg(); + testInvalidArgToolProvider(); + } + +} diff --git a/test/jdk/tools/jpackage/share/JavaOptionsBase.java b/test/jdk/tools/jpackage/share/JavaOptionsBase.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/JavaOptionsBase.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +public class JavaOptionsBase { + + private static final String app = JPackagePath.getApp(); + private static final String appOutput = JPackagePath.getAppOutputFile(); + + private static final String ARGUMENT1 = "-Dparam1=Some Param 1"; + private static final String ARGUMENT2 = "-Dparam2=Some \"Param\" 2"; + private static final String ARGUMENT3 = + "-Dparam3=Some \"Param\" with \" 3"; + + private static final List arguments = new ArrayList<>(); + + private static void initArguments(boolean toolProvider, String [] cmd) { + if (arguments.isEmpty()) { + arguments.add(ARGUMENT1); + arguments.add(ARGUMENT2); + arguments.add(ARGUMENT3); + } + + String argumentsMap = JPackageHelper.listToArgumentsMap(arguments, + toolProvider); + cmd[cmd.length - 1] = argumentsMap; + } + + private static void initArguments2(boolean toolProvider, String [] cmd) { + int index = cmd.length - 6; + + cmd[index++] = "--java-options"; + arguments.clear(); + arguments.add(ARGUMENT1); + cmd[index++] = JPackageHelper.listToArgumentsMap(arguments, + toolProvider); + + cmd[index++] = "--java-options"; + arguments.clear(); + arguments.add(ARGUMENT2); + cmd[index++] = JPackageHelper.listToArgumentsMap(arguments, + toolProvider); + + cmd[index++] = "--java-options"; + arguments.clear(); + arguments.add(ARGUMENT3); + cmd[index++] = JPackageHelper.listToArgumentsMap(arguments, + toolProvider); + + arguments.clear(); + arguments.add(ARGUMENT1); + arguments.add(ARGUMENT2); + arguments.add(ARGUMENT3); + } + + private static void validateResult(String[] result, List args) + throws Exception { + if (result.length != (args.size() + 2)) { + for (String r : result) { + System.err.println(r.trim()); + } + throw new AssertionError("Unexpected number of lines: " + + result.length); + } + + if (!result[0].trim().equals("jpackage test application")) { + throw new AssertionError("Unexpected result[0]: " + result[0]); + } + + if (!result[1].trim().equals("args.length: 0")) { + throw new AssertionError("Unexpected result[1]: " + result[1]); + } + + int index = 2; + for (String arg : args) { + if (!result[index].trim().equals(arg)) { + throw new AssertionError("Unexpected result[" + index + "]: " + + result[index]); + } + index++; + } + } + + private static void validate(List expectedArgs) throws Exception { + int retVal = JPackageHelper.execute(null, app); + if (retVal != 0) { + throw new AssertionError("Test application exited with error: " + + retVal); + } + + File outfile = new File(appOutput); + if (!outfile.exists()) { + throw new AssertionError(appOutput + " was not created"); + } + + String output = Files.readString(outfile.toPath()); + String[] result = JPackageHelper.splitAndFilter(output); + validateResult(result, expectedArgs); + } + + public static void testCreateAppImageJavaOptions(String [] cmd) throws Exception { + initArguments(false, cmd); + JPackageHelper.executeCLI(true, cmd); + validate(arguments); + } + + public static void testCreateAppImageJavaOptionsToolProvider(String [] cmd) throws Exception { + initArguments(true, cmd); + JPackageHelper.executeToolProvider(true, cmd); + validate(arguments); + } + + public static void testCreateAppImageJavaOptions2(String [] cmd) throws Exception { + initArguments2(false, cmd); + JPackageHelper.executeCLI(true, cmd); + validate(arguments); + } + + public static void testCreateAppImageJavaOptions2ToolProvider(String [] cmd) throws Exception { + initArguments2(true, cmd); + JPackageHelper.executeToolProvider(true, cmd); + validate(arguments); + } +} diff --git a/test/jdk/tools/jpackage/share/JavaOptionsEqualsTest.java b/test/jdk/tools/jpackage/share/JavaOptionsEqualsTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/JavaOptionsEqualsTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.File; +import java.nio.file.Files; + +/* + * @test + * @summary jpackage create image with --java-options test + * @library ../helpers + * @build JPackageHelper + * @build JPackagePath + * @build JavaOptionsBase + * @modules jdk.incubator.jpackage + * @run main/othervm -Xmx512m JavaOptionsEqualsTest + */ +public class JavaOptionsEqualsTest { + + private static final String app = JPackagePath.getApp(); + + private static final String OUTPUT = "output"; + + private static final String WARNING_1 + = "WARNING: Unknown module: me.mymodule.foo"; + + private static final String WARNING_2 + = "WARNING: Unknown module: other.mod.bar"; + + private static final String[] CMD = { + "--type", "app-image", + "--input", "input", + "--description", "the two options below should cause two app execution " + + "Warnings with two lines output saying: " + + "WARNING: Unknown module: ", + "--dest", OUTPUT, + "--name", "test", + "--main-jar", "hello.jar", + "--main-class", "Hello", + "--java-options", + "--add-exports=java.base/sun.util=me.mymodule.foo,ALL-UNNAMED", + "--java-options", + "--add-exports=java.base/sun.security.util=other.mod.bar,ALL-UNNAMED", + }; + + private static void validate() throws Exception { + File outfile = new File("app.out"); + + int retVal = JPackageHelper.execute(outfile, app); + if (retVal != 0) { + throw new AssertionError( + "Test application exited with error: " + retVal); + } + + if (!outfile.exists()) { + throw new AssertionError( + "outfile: " + outfile + " was not created"); + } + + String output = Files.readString(outfile.toPath()); + System.out.println("App output:"); + System.out.print(output); + + String[] result = JPackageHelper.splitAndFilter(output); + if (result.length != 4) { + throw new AssertionError( + "Unexpected number of lines: " + result.length + + " - output: " + output); + } + + String nextWarning = WARNING_1; + if (!result[0].startsWith(nextWarning)){ + nextWarning = WARNING_2; + if (!result[0].startsWith(WARNING_2)){ + throw new AssertionError("Unexpected result[0]: " + result[0]); + } else { + nextWarning = WARNING_1; + } + } + + if (!result[1].startsWith(nextWarning)) { + throw new AssertionError("Unexpected result[1]: " + result[1]); + } + + if (!result[2].trim().endsWith("jpackage test application")) { + throw new AssertionError("Unexpected result[2]: " + result[2]); + } + + if (!result[3].trim().equals("args.length: 0")) { + throw new AssertionError("Unexpected result[3]: " + result[3]); + } + } + + public static void main(String[] args) throws Exception { + JPackageHelper.createHelloImageJar(); + String output = JPackageHelper.executeCLI(true, CMD); + validate(); + + JPackageHelper.deleteOutputFolder(OUTPUT); + output = JPackageHelper.executeToolProvider(true, CMD); + validate(); + } + +} diff --git a/test/jdk/tools/jpackage/share/JavaOptionsModuleTest.java b/test/jdk/tools/jpackage/share/JavaOptionsModuleTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/JavaOptionsModuleTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary jpackage create image with --java-options test + * @library ../helpers + * @build JPackageHelper + * @build JPackagePath + * @build JavaOptionsBase + * @modules jdk.incubator.jpackage + * @run main/othervm -Xmx512m JavaOptionsModuleTest + */ +public class JavaOptionsModuleTest { + private static final String OUTPUT = "output"; + + private static final String[] CMD = { + "--type", "app-image", + "--dest", OUTPUT, + "--name", "test", + "--module", "com.hello/com.hello.Hello", + "--module-path", "input", + "--java-options", "TBD"}; + + private static final String[] CMD2 = { + "--type", "app-image", + "--dest", OUTPUT, + "--name", "test", + "--module", "com.hello/com.hello.Hello", + "--module-path", "input", + "--java-options", "TBD", + "--java-options", "TBD", + "--java-options", "TBD"}; + + public static void main(String[] args) throws Exception { + JPackageHelper.createHelloModule(); + + JavaOptionsBase.testCreateAppImageJavaOptions(CMD); + JPackageHelper.deleteOutputFolder(OUTPUT); + JavaOptionsBase.testCreateAppImageJavaOptionsToolProvider(CMD); + + JPackageHelper.deleteOutputFolder(OUTPUT); + JavaOptionsBase.testCreateAppImageJavaOptions2(CMD2); + JPackageHelper.deleteOutputFolder(OUTPUT); + JavaOptionsBase.testCreateAppImageJavaOptions2ToolProvider(CMD2); + } + +} diff --git a/test/jdk/tools/jpackage/share/JavaOptionsTest.java b/test/jdk/tools/jpackage/share/JavaOptionsTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/JavaOptionsTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary jpackage create image with --java-options test + * @library ../helpers + * @build JPackageHelper + * @build JPackagePath + * @build JavaOptionsBase + * @modules jdk.incubator.jpackage + * @run main/othervm -Xmx512m JavaOptionsTest + */ +public class JavaOptionsTest { + private static final String OUTPUT = "output"; + + private static final String[] CMD = { + "--type", "app-image", + "--input", "input", + "--dest", OUTPUT, + "--name", "test", + "--main-jar", "hello.jar", + "--main-class", "Hello", + "--java-options", "TBD"}; + + private static final String[] CMD2 = { + "--type", "app-image", + "--input", "input", + "--dest", OUTPUT, + "--name", "test", + "--main-jar", "hello.jar", + "--main-class", "Hello", + "--java-options", "TBD", + "--java-options", "TBD", + "--java-options", "TBD"}; + + public static void main(String[] args) throws Exception { + JPackageHelper.createHelloImageJar(); + JavaOptionsBase.testCreateAppImageJavaOptions(CMD); + JPackageHelper.deleteOutputFolder(OUTPUT); + JavaOptionsBase.testCreateAppImageJavaOptionsToolProvider(CMD); + + JPackageHelper.deleteOutputFolder(OUTPUT); + JavaOptionsBase.testCreateAppImageJavaOptions2(CMD2); + JPackageHelper.deleteOutputFolder(OUTPUT); + JavaOptionsBase.testCreateAppImageJavaOptions2ToolProvider(CMD2); + } + +} diff --git a/test/jdk/tools/jpackage/share/LicenseTest.java b/test/jdk/tools/jpackage/share/LicenseTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/LicenseTest.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.function.Function; +import java.util.stream.Collectors; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.LinuxHelper; +import jdk.jpackage.test.Executor; +import jdk.jpackage.test.TKit; + +/** + * Test --license-file parameter. Output of the test should be commonlicensetest*.* + * package bundle. The output package should provide the same functionality as + * the default package and also incorporate license information from + * test/jdk/tools/jpackage/resources/license.txt file from OpenJDK repo. + * + * deb: + * + * Package should install license file /opt/commonlicensetest/share/doc/copyright + * file. + * + * rpm: + * + * Package should install license file in + * %{_defaultlicensedir}/licensetest-1.0/license.txt file. + * + * Mac: + * + * Windows + * + * Installer should display license text matching contents of the license file + * during installation. + */ + +/* + * @test + * @summary jpackage with --license-file + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @compile LicenseTest.java + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=LicenseTest.testCommon + */ + +/* + * @test + * @summary jpackage with --license-file + * @library ../helpers + * @key jpackagePlatformPackage + * @compile LicenseTest.java + * @requires (os.family == "linux") + * @requires (jpackage.test.SQETest == null) + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=LicenseTest.testCustomDebianCopyright + * --jpt-run=LicenseTest.testCustomDebianCopyrightSubst + */ + +public class LicenseTest { + public static void testCommon() { + new PackageTest().configureHelloApp() + .addInitializer(cmd -> { + cmd.addArguments("--license-file", TKit.createRelativePathCopy( + LICENSE_FILE)); + }) + .forTypes(PackageType.LINUX) + .addBundleVerifier(cmd -> { + verifyLicenseFileInLinuxPackage(cmd, linuxLicenseFile(cmd)); + }) + .addInstallVerifier(cmd -> { + TKit.assertReadableFileExists(linuxLicenseFile(cmd)); + }) + .addUninstallVerifier(cmd -> { + verifyLicenseFileNotInstalledLinux(linuxLicenseFile(cmd)); + }) + .forTypes(PackageType.LINUX_DEB) + .addInstallVerifier(cmd -> { + verifyLicenseFileInstalledDebian(debLicenseFile(cmd)); + }) + .forTypes(PackageType.LINUX_RPM) + .addInstallVerifier(cmd -> { + verifyLicenseFileInstalledRpm(rpmLicenseFile(cmd)); + }) + .run(); + } + + public static void testCustomDebianCopyright() { + new CustomDebianCopyrightTest().run(); + } + + public static void testCustomDebianCopyrightSubst() { + new CustomDebianCopyrightTest().withSubstitution(true).run(); + } + + private static Path rpmLicenseFile(JPackageCommand cmd) { + final Path licenseRoot = Path.of( + new Executor() + .setExecutable("rpm") + .addArguments("--eval", "%{_defaultlicensedir}") + .executeAndGetFirstLineOfOutput()); + final Path licensePath = licenseRoot.resolve(String.format("%s-%s", + LinuxHelper.getPackageName(cmd), cmd.version())).resolve( + LICENSE_FILE.getFileName()); + return licensePath; + } + + private static Path linuxLicenseFile(JPackageCommand cmd) { + cmd.verifyIsOfType(PackageType.LINUX); + switch (cmd.packageType()) { + case LINUX_DEB: + return debLicenseFile(cmd); + + case LINUX_RPM: + return rpmLicenseFile(cmd); + + default: + return null; + } + } + + private static Path debLicenseFile(JPackageCommand cmd) { + return cmd.appInstallationDirectory().resolve("share/doc/copyright"); + } + + private static void verifyLicenseFileInLinuxPackage(JPackageCommand cmd, + Path expectedLicensePath) { + TKit.assertTrue(LinuxHelper.getPackageFiles(cmd).filter(path -> path.equals( + expectedLicensePath)).findFirst().orElse(null) != null, + String.format("Check license file [%s] is in %s package", + expectedLicensePath, LinuxHelper.getPackageName(cmd))); + } + + private static void verifyLicenseFileInstalledRpm(Path licenseFile) throws + IOException { + TKit.assertStringListEquals(Files.readAllLines(LICENSE_FILE), + Files.readAllLines(licenseFile), String.format( + "Check contents of package license file [%s] are the same as contents of source license file [%s]", + licenseFile, LICENSE_FILE)); + } + + private static void verifyLicenseFileInstalledDebian(Path licenseFile) + throws IOException { + + List actualLines = Files.readAllLines(licenseFile).stream().dropWhile( + line -> !line.startsWith("License:")).collect( + Collectors.toList()); + // Remove leading `License:` followed by the whitespace from the first text line. + actualLines.set(0, actualLines.get(0).split("\\s+", 2)[1]); + + actualLines = DEBIAN_COPYRIGT_FILE_STRIPPER.apply(actualLines); + + TKit.assertNotEquals(0, String.join("\n", actualLines).length(), + "Check stripped license text is not empty"); + + TKit.assertStringListEquals(DEBIAN_COPYRIGT_FILE_STRIPPER.apply( + Files.readAllLines(LICENSE_FILE)), actualLines, String.format( + "Check subset of package license file [%s] is a match of the source license file [%s]", + licenseFile, LICENSE_FILE)); + } + + private static void verifyLicenseFileNotInstalledLinux(Path licenseFile) { + TKit.assertPathExists(licenseFile.getParent(), false); + } + + private static class CustomDebianCopyrightTest { + CustomDebianCopyrightTest() { + withSubstitution(false); + } + + private List licenseFileText(String copyright, String licenseText) { + List lines = new ArrayList(List.of( + String.format("Copyright=%s", copyright), + "Foo", + "Bar", + "Buz")); + lines.addAll(List.of(licenseText.split("\\R", -1))); + return lines; + } + + private List licenseFileText() { + if (withSubstitution) { + return licenseFileText("APPLICATION_COPYRIGHT", + "APPLICATION_LICENSE_TEXT"); + } else { + return expetedLicenseFileText(); + } + } + + private List expetedLicenseFileText() { + return licenseFileText(copyright, licenseText); + } + + CustomDebianCopyrightTest withSubstitution(boolean v) { + withSubstitution = v; + // Different values just to make easy to figure out from the test log which test was executed. + if (v) { + copyright = "Duke (C)"; + licenseText = "The quick brown fox\n jumps over the lazy dog"; + } else { + copyright = "Java (C)"; + licenseText = "How vexingly quick daft zebras jump!"; + } + return this; + } + + void run() { + final Path srcLicenseFile = TKit.workDir().resolve("license"); + new PackageTest().configureHelloApp().forTypes(PackageType.LINUX_DEB) + .addInitializer(cmd -> { + // Create source license file. + Files.write(srcLicenseFile, List.of( + licenseText.split("\\R", -1))); + + cmd.setFakeRuntime(); + cmd.setArgumentValue("--name", String.format("%s%s", + withSubstitution ? "CustomDebianCopyrightWithSubst" : "CustomDebianCopyright", + cmd.name())); + cmd.addArguments("--license-file", srcLicenseFile); + cmd.addArguments("--copyright", copyright); + cmd.addArguments("--resource-dir", RESOURCE_DIR); + + // Create copyright template file in a resource dir. + Files.createDirectories(RESOURCE_DIR); + Files.write(RESOURCE_DIR.resolve("copyright"), + licenseFileText()); + }) + .addInstallVerifier(cmd -> { + Path installedLicenseFile = debLicenseFile(cmd); + TKit.assertStringListEquals(expetedLicenseFileText(), + DEBIAN_COPYRIGT_FILE_STRIPPER.apply(Files.readAllLines( + installedLicenseFile)), String.format( + "Check contents of package license file [%s] are the same as contents of source license file [%s]", + installedLicenseFile, srcLicenseFile)); + }) + .run(); + } + + private boolean withSubstitution; + private String copyright; + private String licenseText; + + private final Path RESOURCE_DIR = TKit.workDir().resolve("resources"); + } + + private static final Path LICENSE_FILE = TKit.TEST_SRC_ROOT.resolve( + Path.of("resources", "license.txt")); + + private static final Function, List> DEBIAN_COPYRIGT_FILE_STRIPPER = (lines) -> Arrays.asList( + String.join("\n", lines).stripTrailing().split("\n")); +} diff --git a/test/jdk/tools/jpackage/share/MissingArgumentsTest.java b/test/jdk/tools/jpackage/share/MissingArgumentsTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/MissingArgumentsTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + /* + * @test + * @summary jpackage create image missing arguments test + * @library ../helpers + * @build JPackageHelper + * @build JPackagePath + * @modules jdk.incubator.jpackage + * @run main/othervm -Xmx512m MissingArgumentsTest + */ + +public class MissingArgumentsTest { + private static final String [] RESULT_1 = {"--input"}; + private static final String [] CMD_1 = { + "--type", "app-image", + "--dest", "output", + "--name", "test", + "--main-jar", "hello.jar", + "--main-class", "Hello", + }; + + private static final String [] RESULT_2 = {"--input", "--app-image"}; + private static final String [] CMD_2 = { + "--type", "app-image", + "--type", "invalid-type", + "--dest", "output", + "--name", "test", + "--main-jar", "hello.jar", + "--main-class", "Hello", + }; + + private static final String [] RESULT_3 = {"main class was not specified"}; + private static final String [] CMD_3 = { + "--type", "app-image", + "--input", "input", + "--dest", "output", + "--name", "test", + "--main-jar", "hello.jar", + }; + + private static final String [] RESULT_4 = {"--main-jar"}; + private static final String [] CMD_4 = { + "--type", "app-image", + "--input", "input", + "--dest", "output", + "--name", "test", + "--main-class", "Hello", + }; + + private static final String [] RESULT_5 = {"--module-path", "--runtime-image"}; + private static final String [] CMD_5 = { + "--type", "app-image", + "--dest", "output", + "--name", "test", + "--module", "com.hello/com.hello.Hello", + }; + + private static final String [] RESULT_6 = {"--module-path", "--runtime-image", + "--app-image"}; + private static final String [] CMD_6 = { + "--type", "invalid-type", + "--dest", "output", + "--name", "test", + "--module", "com.hello/com.hello.Hello", + }; + + private static void validate(String output, String [] expected, + boolean single) throws Exception { + String[] result = JPackageHelper.splitAndFilter(output); + if (single && result.length != 1) { + System.err.println(output); + throw new AssertionError("Invalid number of lines in output: " + + result.length); + } + + for (String s : expected) { + if (!result[0].contains(s)) { + System.err.println("Expected to contain: " + s); + System.err.println("Actual: " + result[0]); + throw new AssertionError("Unexpected error message"); + } + } + } + + private static void testMissingArg() throws Exception { + String output = JPackageHelper.executeCLI(false, CMD_1); + validate(output, RESULT_1, true); + + output = JPackageHelper.executeCLI(false, CMD_2); + validate(output, RESULT_2, true); + + output = JPackageHelper.executeCLI(false, CMD_3); + validate(output, RESULT_3, false); + + output = JPackageHelper.executeCLI(false, CMD_4); + validate(output, RESULT_4, true); + + output = JPackageHelper.executeCLI(false, CMD_5); + validate(output, RESULT_5, true); + + output = JPackageHelper.executeCLI(false, CMD_6); + validate(output, RESULT_6, true); + + } + + private static void testMissingArgToolProvider() throws Exception { + String output = JPackageHelper.executeToolProvider(false, CMD_1); + validate(output, RESULT_1, true); + + output = JPackageHelper.executeToolProvider(false, CMD_2); + validate(output, RESULT_2, true); + + output = JPackageHelper.executeToolProvider(false, CMD_3); + validate(output, RESULT_3, false); + + output = JPackageHelper.executeToolProvider(false, CMD_4); + validate(output, RESULT_4, true); + + output = JPackageHelper.executeToolProvider(false, CMD_5); + validate(output, RESULT_5, true); + + output = JPackageHelper.executeToolProvider(false, CMD_6); + validate(output, RESULT_6, true); + } + + public static void main(String[] args) throws Exception { + JPackageHelper.createHelloImageJar(); + testMissingArg(); + testMissingArgToolProvider(); + } + +} diff --git a/test/jdk/tools/jpackage/share/RuntimePackageTest.java b/test/jdk/tools/jpackage/share/RuntimePackageTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/RuntimePackageTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import jdk.jpackage.test.*; +import jdk.jpackage.test.Annotations.Test; + +/** + * Test --runtime-image parameter. + * Output of the test should be RuntimePackageTest*.* installer. + * The installer should install Java Runtime without an application. + * Installation directory should not have "app" subfolder and should not have + * an application launcher. + * + * + * Windows: + * + * Java runtime should be installed in %ProgramFiles%\RuntimePackageTest directory. + */ + +/* + * @test + * @summary jpackage with --runtime-image + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @comment Temporary disable for Linux and OSX until functionality implemented + * @requires (os.family != "mac") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile RuntimePackageTest.java + * @run main/othervm/timeout=720 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=RuntimePackageTest + */ +public class RuntimePackageTest { + + @Test + public static void test() { + new PackageTest() + .addInitializer(cmd -> { + cmd.addArguments("--runtime-image", Optional.ofNullable( + JPackageCommand.DEFAULT_RUNTIME_IMAGE).orElse(Path.of( + System.getProperty("java.home")))); + // Remove --input parameter from jpackage command line as we don't + // create input directory in the test and jpackage fails + // if --input references non existant directory. + cmd.removeArgumentWithValue("--input"); + }) + .addInstallVerifier(cmd -> { + Set srcRuntime = listFiles(Path.of(cmd.getArgumentValue("--runtime-image"))); + Set dstRuntime = listFiles(cmd.appRuntimeDirectory()); + + Set intersection = new HashSet<>(srcRuntime); + intersection.retainAll(dstRuntime); + + srcRuntime.removeAll(intersection); + dstRuntime.removeAll(intersection); + + assertFileListEmpty(srcRuntime, "Missing"); + assertFileListEmpty(dstRuntime, "Unexpected"); + }) + .run(); + } + + private static Set listFiles(Path root) throws IOException { + try (var files = Files.walk(root)) { + return files.map(root::relativize).collect(Collectors.toSet()); + } + } + + private static void assertFileListEmpty(Set paths, String msg) { + TKit.assertTrue(paths.isEmpty(), String.format( + "Check there are no %s files in installed image", + msg.toLowerCase()), () -> { + String msg2 = String.format("%s %d files", msg, paths.size()); + TKit.trace(msg2 + ":"); + paths.stream().map(Path::toString).sorted().forEachOrdered( + TKit::trace); + TKit.trace("Done"); + }); + } +} diff --git a/test/jdk/tools/jpackage/share/SimplePackageTest.java b/test/jdk/tools/jpackage/share/SimplePackageTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/SimplePackageTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.Annotations.Test; + +/** + * Simple platform specific packaging test. Output of the test should be + * simplepackagetest*.* package bundle. + * + * Windows: + * + * The installer should not have license text. It should not have an option + * to change the default installation directory. + * Test application should be installed in %ProgramFiles%\SimplePackageTest directory. + * Installer should install test app for all users (machine wide). + * Installer should not create any shortcuts. + */ + +/* + * @test + * @summary Simple jpackage command run + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile SimplePackageTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=SimplePackageTest + */ +public class SimplePackageTest { + + @Test + public static void test() { + new PackageTest() + .configureHelloApp() + .addBundleDesktopIntegrationVerifier(false) + .run(); + } +} diff --git a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/AppVersionTest.java b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/AppVersionTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/AppVersionTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.tests; + +import java.util.Collection; +import java.util.List; +import jdk.jpackage.test.Annotations.Parameters; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.TKit; + +/* + * @test + * @summary jpackage application version testing + * @library ../../../../helpers + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile AppVersionTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=jdk.jpackage.tests.AppVersionTest + */ + +public final class AppVersionTest { + + @Parameters + public static Collection input() { + return List.of(new Object[][]{ + // Default jpackage version + {"1.0", "Hello", null}, + {"1.0", "com.other/com.other.Hello", null}, + // Version should be picked from --app-version + {"3.1", "Hello", new String[]{"--app-version", "3.1"}}, + {"3.2", "com.other/com.other.Hello", new String[]{"--app-version", + "3.2"}}, + // Version should be picked from the last --app-version + {"3.3", "Hello", new String[]{"--app-version", "4", "--app-version", + "3.3"}}, + {"7.8", "com.other/com.other.Hello", new String[]{"--app-version", + "4", "--app-version", "7.8"}}, + // Pick version from jar + {"3.10.17", "com.other/com.other.Hello@3.10.17", null}, + // Ignore version in jar if --app-version given + {"7.5.81", "com.other/com.other.Hello@3.10.17", new String[]{ + "--app-version", "7.5.81"}} + }); + } + + public AppVersionTest(String expectedVersion, String javaAppDesc, + String[] jpackageArgs) { + this.expectedVersion = expectedVersion; + + cmd = JPackageCommand.helloAppImage(javaAppDesc); + if (jpackageArgs != null) { + cmd.addArguments(jpackageArgs); + } + } + + @Test + public void test() { + cmd.executeAndAssertHelloAppImageCreated(); + String actualVersion = cmd.readLaunherCfgFile().getValue("Application", + "app.version"); + TKit.assertEquals(expectedVersion, actualVersion, + "Check application version"); + } + + private final String expectedVersion; + private final JPackageCommand cmd; +} diff --git a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.tests; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.ArrayList; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.test.*; +import jdk.jpackage.test.Functional.ThrowingConsumer; +import jdk.jpackage.test.Annotations.*; + +/* + * @test + * @summary jpackage basic testing + * @library ../../../../helpers + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile BasicTest.java + * @run main/othervm/timeout=720 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=jdk.jpackage.tests.BasicTest + */ + +public final class BasicTest { + @Test + public void testNoArgs() { + List output = + getJPackageToolProvider().executeAndGetOutput(); + TKit.assertStringListEquals(List.of("Usage: jpackage ", + "Use jpackage --help (or -h) for a list of possible options"), + output, "Check jpackage output"); + } + + @Test + public void testVersion() { + List output = + getJPackageToolProvider() + .addArgument("--version") + .executeAndGetOutput(); + TKit.assertStringListEquals(List.of(System.getProperty("java.version")), + output, "Check jpackage output"); + } + + @Test + public void testHelp() { + List hOutput = getJPackageToolProvider() + .addArgument("-h").executeAndGetOutput(); + List helpOutput = getJPackageToolProvider() + .addArgument("--help").executeAndGetOutput(); + + TKit.assertStringListEquals(hOutput, helpOutput, + "Check -h and --help parameters produce the same output"); + + final String windowsPrefix = "--win-"; + final String linuxPrefix = "--linux-"; + final String osxPrefix = "--mac-"; + + final String expectedPrefix; + final List unexpectedPrefixes; + + if (TKit.isWindows()) { + expectedPrefix = windowsPrefix; + unexpectedPrefixes = List.of(osxPrefix, linuxPrefix); + } else if (TKit.isLinux()) { + expectedPrefix = linuxPrefix; + unexpectedPrefixes = List.of(windowsPrefix, osxPrefix); + } else if (TKit.isOSX()) { + expectedPrefix = osxPrefix; + unexpectedPrefixes = List.of(linuxPrefix, windowsPrefix); + } else { + throw TKit.throwUnknownPlatformError(); + } + + Function> createPattern = (prefix) -> { + return Pattern.compile("^ " + prefix).asPredicate(); + }; + + Function, Long> countStrings = (prefixes) -> { + return hOutput.stream().filter( + prefixes.stream().map(createPattern).reduce(x -> false, + Predicate::or)).peek(TKit::trace).count(); + }; + + TKit.trace("Check parameters in help text"); + TKit.assertNotEquals(0, countStrings.apply(List.of(expectedPrefix)), + "Check help text contains plaform specific parameters"); + TKit.assertEquals(0, countStrings.apply(unexpectedPrefixes), + "Check help text doesn't contain unexpected parameters"); + } + + @Test + @SuppressWarnings("unchecked") + public void testVerbose() { + JPackageCommand cmd = JPackageCommand.helloAppImage() + .setFakeRuntime().executePrerequisiteActions(); + + List expectedVerboseOutputStrings = new ArrayList<>(); + expectedVerboseOutputStrings.add("Creating app package:"); + if (TKit.isWindows()) { + expectedVerboseOutputStrings.add("Result application bundle:"); + expectedVerboseOutputStrings.add( + "Succeeded in building Windows Application Image package"); + } else if (TKit.isLinux()) { + expectedVerboseOutputStrings.add( + "Succeeded in building Linux Application Image package"); + } else if (TKit.isOSX()) { + expectedVerboseOutputStrings.add("Preparing Info.plist:"); + expectedVerboseOutputStrings.add( + "Succeeded in building Mac Application Image package"); + } else { + TKit.throwUnknownPlatformError(); + } + + TKit.deleteDirectoryContentsRecursive(cmd.outputDir()); + List nonVerboseOutput = cmd.createExecutor().executeAndGetOutput(); + List[] verboseOutput = (List[])new List[1]; + + // Directory clean up is not 100% reliable on Windows because of + // antivirus software that can lock .exe files. Setup + // diffreent output directory instead of cleaning the default one for + // verbose jpackage run. + TKit.withTempDirectory("verbose-output", tempDir -> { + cmd.setArgumentValue("--dest", tempDir); + verboseOutput[0] = cmd.createExecutor().addArgument( + "--verbose").executeAndGetOutput(); + }); + + TKit.assertTrue(nonVerboseOutput.size() < verboseOutput[0].size(), + "Check verbose output is longer than regular"); + + expectedVerboseOutputStrings.forEach(str -> { + TKit.assertTextStream(str).label("regular output") + .predicate(String::contains).negate() + .apply(nonVerboseOutput.stream()); + }); + + expectedVerboseOutputStrings.forEach(str -> { + TKit.assertTextStream(str).label("verbose output") + .apply(verboseOutput[0].stream()); + }); + } + + @Test + public void testNoName() { + final String mainClassName = "Greetings"; + + JPackageCommand cmd = JPackageCommand.helloAppImage(mainClassName) + .removeArgumentWithValue("--name"); + + Path expectedImageDir = cmd.outputDir().resolve(mainClassName); + if (TKit.isOSX()) { + expectedImageDir = expectedImageDir.getParent().resolve( + expectedImageDir.getFileName().toString() + ".app"); + } + + cmd.executeAndAssertHelloAppImageCreated(); + TKit.assertEquals(expectedImageDir.toAbsolutePath().normalize().toString(), + cmd.outputBundle().toAbsolutePath().normalize().toString(), + String.format( + "Check [%s] directory is filled with application image data", + expectedImageDir)); + } + + @Test + // Regular app + @Parameter("Hello") + // Modular app + @Parameter("com.other/com.other.Hello") + public void testApp(String javaAppDesc) { + JPackageCommand.helloAppImage(javaAppDesc) + .executeAndAssertHelloAppImageCreated(); + } + + @Test + public void testWhitespaceInPaths() { + JPackageCommand.helloAppImage("a/b c.jar:Hello") + .setArgumentValue("--input", TKit.workDir().resolve("The quick brown fox")) + .setArgumentValue("--dest", TKit.workDir().resolve("jumps over the lazy dog")) + .executeAndAssertHelloAppImageCreated(); + } + + @Test + @Parameter("ALL-MODULE-PATH") + @Parameter("ALL-DEFAULT") + @Parameter("java.desktop") + @Parameter("java.desktop,jdk.jartool") + @Parameter({ "java.desktop", "jdk.jartool" }) + public void testAddModules(String... addModulesArg) { + JPackageCommand cmd = JPackageCommand + .helloAppImage("goodbye.jar:com.other/com.other.Hello"); + Stream.of(addModulesArg).map(v -> Stream.of("--add-modules", v)).flatMap( + s -> s).forEachOrdered(cmd::addArgument); + cmd.executeAndAssertHelloAppImageCreated(); + } + + /** + * Test --temp option. Doesn't make much sense for app image as temporary + * directory is used only on Windows. Test it in packaging mode. + * @throws IOException + */ + @Test + public void testTemp() throws IOException { + TKit.withTempDirectory("temp-root", tempRoot -> { + Function getTempDir = cmd -> { + return tempRoot.resolve(cmd.outputBundle().getFileName()); + }; + + ThrowingConsumer addTempDir = cmd -> { + Path tempDir = getTempDir.apply(cmd); + Files.createDirectories(tempDir); + cmd.addArguments("--temp", tempDir); + }; + + new PackageTest().configureHelloApp().addInitializer(addTempDir) + .addBundleVerifier(cmd -> { + // Check jpackage actually used the supplied directory. + Path tempDir = getTempDir.apply(cmd); + TKit.assertNotEquals(0, tempDir.toFile().list().length, + String.format( + "Check jpackage wrote some data in the supplied temporary directory [%s]", + tempDir)); + }) + .run(); + + new PackageTest().configureHelloApp().addInitializer(addTempDir) + .addInitializer(cmd -> { + // Clean output from the previus jpackage run. + Files.delete(cmd.outputBundle()); + }) + // Temporary directory should not be empty, + // jpackage should exit with error. + .setExpectedExitCode(1) + .run(); + }); + } + + @Test + public void testAtFile() throws IOException { + JPackageCommand cmd = JPackageCommand.helloAppImage(); + + // Init options file with the list of options configured + // for JPackageCommand instance. + final Path optionsFile = TKit.workDir().resolve("options"); + Files.write(optionsFile, + List.of(String.join(" ", cmd.getAllArguments()))); + + // Build app jar file. + cmd.executePrerequisiteActions(); + + // Make sure output directory is empty. Normally JPackageCommand would + // do this automatically. + TKit.deleteDirectoryContentsRecursive(cmd.outputDir()); + + // Instead of running jpackage command through configured + // JPackageCommand instance, run vanilla jpackage command with @ file. + getJPackageToolProvider() + .addArgument(String.format("@%s", optionsFile)) + .execute().assertExitCodeIsZero(); + + // Verify output of jpackage command. + cmd.assertImageCreated(); + HelloApp.executeLauncherAndVerifyOutput(cmd); + } + + @Parameter("Hello") + @Parameter("com.foo/com.foo.main.Aloha") + @Test + public void testJLinkRuntime(String javaAppDesc) { + JPackageCommand cmd = JPackageCommand.helloAppImage(javaAppDesc); + + // If `--module` parameter was set on jpackage command line, get its + // value and extract module name. + // E.g.: foo.bar2/foo.bar.Buz -> foo.bar2 + // Note: HelloApp class manages `--module` parameter on jpackage command line + final String moduleName = cmd.getArgumentValue("--module", () -> null, + (v) -> v.split("/", 2)[0]); + + if (moduleName != null) { + // Build module jar. + cmd.executePrerequisiteActions(); + } + + TKit.withTempDirectory("runtime", tempDir -> { + final Path runtimeDir = tempDir.resolve("data"); + + // List of modules required for test app. + final var modules = new String[] { + "java.base", + "java.desktop" + }; + + Executor jlink = getToolProvider(JavaTool.JLINK) + .saveOutput(false) + .addArguments( + "--add-modules", String.join(",", modules), + "--output", runtimeDir.toString(), + "--strip-debug", + "--no-header-files", + "--no-man-pages"); + + if (moduleName != null) { + jlink.addArguments("--add-modules", moduleName, "--module-path", + Path.of(cmd.getArgumentValue("--module-path")).resolve( + "hello.jar").toString()); + } + + jlink.execute().assertExitCodeIsZero(); + + cmd.addArguments("--runtime-image", runtimeDir); + cmd.executeAndAssertHelloAppImageCreated(); + }); + } + + private static Executor getJPackageToolProvider() { + return getToolProvider(JavaTool.JPACKAGE); + } + + private static Executor getToolProvider(JavaTool tool) { + return new Executor().dumpOutput().saveOutput().setToolProvider(tool); + } +} diff --git a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.tests; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collection; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.nio.file.Path; +import java.util.function.Predicate; +import java.util.jar.JarEntry; +import jdk.jpackage.test.Annotations.Parameters; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.*; +import jdk.jpackage.test.Functional.ThrowingConsumer; +import static jdk.jpackage.tests.MainClassTest.Script.MainClassType.*; + + +/* + * @test + * @summary test different settings of main class name for jpackage + * @library ../../../../helpers + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile MainClassTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=jdk.jpackage.tests.MainClassTest + */ + +public final class MainClassTest { + + static final class Script { + Script() { + appDesc = JavaAppDesc.parse("test.Hello"); + } + + Script modular(boolean v) { + appDesc.setModuleName(v ? "com.other" : null); + return this; + } + + Script withJLink(boolean v) { + withJLink = v; + return this; + } + + Script withMainClass(MainClassType v) { + mainClass = v; + return this; + } + + Script withJarMainClass(MainClassType v) { + appDesc.setJarWithMainClass(v != NotSet); + jarMainClass = v; + return this; + } + + Script expectedErrorMessage(String v) { + expectedErrorMessage = v; + return this; + } + + @Override + public String toString() { + return Stream.of( + format("modular", appDesc.moduleName() != null ? 'y' : 'n'), + format("main-class", mainClass), + format("jar-main-class", jarMainClass), + format("jlink", withJLink ? 'y' : 'n'), + format("error", expectedErrorMessage) + ).filter(Objects::nonNull).collect(Collectors.joining("; ")); + } + + private static String format(String key, Object value) { + if (value == null) { + return null; + } + return String.join("=", key, value.toString()); + } + + enum MainClassType { + NotSet("n"), + SetWrong("b"), + SetRight("y"); + + MainClassType(String label) { + this.label = label; + } + + @Override + public String toString() { + return label; + } + + private final String label; + }; + + private JavaAppDesc appDesc; + private boolean withJLink; + private MainClassType mainClass; + private MainClassType jarMainClass; + private String expectedErrorMessage; + } + + public MainClassTest(Script script) { + this.script = script; + + nonExistingMainClass = Stream.of( + script.appDesc.packageName(), "ThereIsNoSuchClass").filter( + Objects::nonNull).collect(Collectors.joining(".")); + + cmd = JPackageCommand + .helloAppImage(script.appDesc) + .ignoreDefaultRuntime(true); + if (!script.withJLink) { + cmd.addArguments("--runtime-image", Path.of(System.getProperty( + "java.home"))); + } + + final String moduleName = script.appDesc.moduleName(); + switch (script.mainClass) { + case NotSet: + if (moduleName != null) { + // Don't specify class name, only module name. + cmd.setArgumentValue("--module", moduleName); + } else { + cmd.removeArgumentWithValue("--main-class"); + } + break; + + case SetWrong: + if (moduleName != null) { + cmd.setArgumentValue("--module", + String.join("/", moduleName, nonExistingMainClass)); + } else { + cmd.setArgumentValue("--main-class", nonExistingMainClass); + } + } + } + + @Parameters + public static Collection scripts() { + final var withMainClass = Set.of(SetWrong, SetRight); + + List scripts = new ArrayList<>(); + for (var withJLink : List.of(true, false)) { + for (var modular : List.of(true, false)) { + for (var mainClass : Script.MainClassType.values()) { + for (var jarMainClass : Script.MainClassType.values()) { + Script script = new Script() + .modular(modular) + .withJLink(withJLink) + .withMainClass(mainClass) + .withJarMainClass(jarMainClass); + + if (withMainClass.contains(jarMainClass) + || withMainClass.contains(mainClass)) { + } else if (modular) { + script.expectedErrorMessage( + "Error: Main application class is missing"); + } else { + script.expectedErrorMessage( + "A main class was not specified nor was one found in the jar"); + } + + scripts.add(new Script[]{script}); + } + } + } + } + return scripts; + } + + @Test + public void test() throws IOException { + if (script.jarMainClass == SetWrong) { + initJarWithWrongMainClass(); + } + + if (script.expectedErrorMessage != null) { + // This is the case when main class is not found nor in jar + // file nor on command line. + List output = cmd + .saveConsoleOutput(true) + .execute() + .assertExitCodeIs(1) + .getOutput(); + TKit.assertTextStream(script.expectedErrorMessage).apply(output.stream()); + return; + } + + // Get here only if main class is specified. + boolean appShouldSucceed = false; + + // Should succeed if valid main class is set on the command line. + appShouldSucceed |= (script.mainClass == SetRight); + + // Should succeed if main class is not set on the command line but set + // to valid value in the jar. + appShouldSucceed |= (script.mainClass == NotSet && script.jarMainClass == SetRight); + + if (appShouldSucceed) { + cmd.executeAndAssertHelloAppImageCreated(); + } else { + cmd.executeAndAssertImageCreated(); + if (!cmd.isFakeRuntime(String.format("Not running [%s]", + cmd.appLauncherPath()))) { + List output = new Executor() + .setDirectory(cmd.outputDir()) + .setExecutable(cmd.appLauncherPath()) + .dumpOutput().saveOutput() + .execute().assertExitCodeIs(1).getOutput(); + TKit.assertTextStream(String.format( + "Error: Could not find or load main class %s", + nonExistingMainClass)).apply(output.stream()); + } + } + } + + private void initJarWithWrongMainClass() throws IOException { + // Call JPackageCommand.executePrerequisiteActions() to build app's jar. + // executePrerequisiteActions() is called by JPackageCommand instance + // only once. + cmd.executePrerequisiteActions(); + + final Path jarFile; + if (script.appDesc.moduleName() != null) { + jarFile = Path.of(cmd.getArgumentValue("--module-path"), + script.appDesc.jarFileName()); + } else { + jarFile = cmd.inputDir().resolve(cmd.getArgumentValue("--main-jar")); + } + + // Create new jar file filtering out main class from the old jar file. + TKit.withTempDirectory("repack-jar", workDir -> { + // Extract app's class from the old jar. + explodeJar(jarFile, workDir, + jarEntry -> Path.of(jarEntry.getName()).equals( + script.appDesc.classFilePath())); + + // Create app's jar file with different main class. + var badAppDesc = JavaAppDesc.parse(script.appDesc.toString()).setClassName( + nonExistingMainClass); + JPackageCommand.helloAppImage(badAppDesc).executePrerequisiteActions(); + + // Extract new jar but skip app's class. + explodeJar(jarFile, workDir, + jarEntry -> !Path.of(jarEntry.getName()).equals( + badAppDesc.classFilePath())); + + // At this point we should have: + // 1. Manifest from the new jar referencing non-existing class + // as the main class. + // 2. Module descriptor referencing non-existing class as the main + // class in case of modular app. + // 3. App's class from the old jar. We need it to let jlink find some + // classes in the package declared in module descriptor + // in case of modular app. + + Files.delete(jarFile); + new Executor().setToolProvider(JavaTool.JAR) + .addArguments("-v", "-c", "-M", "-f", jarFile.toString()) + .addArguments("-C", workDir.toString(), ".") + .dumpOutput() + .execute().assertExitCodeIsZero(); + }); + } + + private static void explodeJar(Path jarFile, Path workDir, + Predicate filter) throws IOException { + try (var jar = new JarFile(jarFile.toFile())) { + jar.stream() + .filter(Predicate.not(JarEntry::isDirectory)) + .filter(filter) + .sequential().forEachOrdered(ThrowingConsumer.toConsumer( + jarEntry -> { + try (var in = jar.getInputStream(jarEntry)) { + Path fileName = workDir.resolve(jarEntry.getName()); + Files.createDirectories(fileName.getParent()); + Files.copy(in, fileName); + } + })); + } + } + + private final JPackageCommand cmd; + private final Script script; + private final String nonExistingMainClass; +} diff --git a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/ModulePathTest.java b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/ModulePathTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/ModulePathTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.tests; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.test.Annotations.Parameters; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.TKit; + + +/* + * @test + * @summary jpackage with --module-path testing + * @library ../../../../helpers + * @build jdk.jpackage.test.* + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile ModulePathTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=jdk.jpackage.tests.ModulePathTest + */ + +public final class ModulePathTest { + + @Parameters + public static Collection data() { + return List.of(new String[][]{ + {GOOD_PATH, EMPTY_DIR, NON_EXISTING_DIR}, + {EMPTY_DIR, NON_EXISTING_DIR, GOOD_PATH}, + {GOOD_PATH + "/a/b/c/d", GOOD_PATH}, + {String.join(File.pathSeparator, EMPTY_DIR, NON_EXISTING_DIR, + GOOD_PATH)}, + {String.join(File.pathSeparator, EMPTY_DIR, NON_EXISTING_DIR), + String.join(File.pathSeparator, EMPTY_DIR, NON_EXISTING_DIR, + GOOD_PATH)}, + {}, + {EMPTY_DIR} + }); + } + + public ModulePathTest(String... modulePathArgs) { + this.modulePathArgs = List.of(modulePathArgs); + } + + @Test + public void test() { + final String moduleName = "com.foo"; + JPackageCommand cmd = JPackageCommand.helloAppImage( + "benvenuto.jar:" + moduleName + "/com.foo.Hello"); + // Build app jar file. + cmd.executePrerequisiteActions(); + + // Ignore runtime that can be set for all tests. Usually if default + // runtime is set, it is fake one to save time on running jlink and + // copying megabytes of data from Java home to application image. + // We need proper runtime for this test. + cmd.ignoreDefaultRuntime(true); + + // --module-path should be set in JPackageCommand.helloAppImage call + String goodModulePath = Objects.requireNonNull(cmd.getArgumentValue( + "--module-path")); + cmd.removeArgumentWithValue("--module-path"); + TKit.withTempDirectory("empty-dir", emptyDir -> { + Path nonExistingDir = TKit.withTempDirectory("non-existing-dir", + unused -> { + }); + + Function substitute = str -> { + String v = str; + v = v.replace(GOOD_PATH, goodModulePath); + v = v.replace(EMPTY_DIR, emptyDir.toString()); + v = v.replace(NON_EXISTING_DIR, nonExistingDir.toString()); + return v; + }; + + boolean withGoodPath = modulePathArgs.stream().anyMatch( + s -> s.contains(GOOD_PATH)); + + cmd.addArguments(modulePathArgs.stream().map(arg -> Stream.of( + "--module-path", substitute.apply(arg))).flatMap(s -> s).collect( + Collectors.toList())); + + if (withGoodPath) { + cmd.executeAndAssertHelloAppImageCreated(); + } else { + final String expectedErrorMessage; + if (modulePathArgs.isEmpty()) { + expectedErrorMessage = "Error: Missing argument: --runtime-image or --module-path"; + } else { + expectedErrorMessage = String.format( + "Error: Module %s not found", moduleName); + } + + List output = cmd + .saveConsoleOutput(true) + .execute() + .assertExitCodeIs(1) + .getOutput(); + TKit.assertTextStream(expectedErrorMessage).apply(output.stream()); + } + }); + } + + private final List modulePathArgs; + + private final static String GOOD_PATH = "@GoodPath@"; + private final static String EMPTY_DIR = "@EmptyDir@"; + private final static String NON_EXISTING_DIR = "@NonExistingDir@"; +} diff --git a/test/jdk/tools/jpackage/test_jpackage.sh b/test/jdk/tools/jpackage/test_jpackage.sh new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/test_jpackage.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# +# Complete testing of jpackage platform-specific packaging. +# +# The script does the following: +# 1. Create packages. +# 2. Install created packages. +# 3. Verifies packages are installed. +# 4. Uninstall created packages. +# 5. Verifies packages are uninstalled. +# +# For the list of accepted command line arguments see `run_tests.sh` script. +# + +# Fail fast +set -e; set -o pipefail; + +# Script debug +dry_run=${JPACKAGE_TEST_DRY_RUN} + +# Default directory where jpackage should write bundle files +output_dir=~/jpackage_bundles + + +set_args () +{ + args=() + local arg_is_output_dir= + local arg_is_mode= + local output_dir_set= + for arg in "$@"; do + if [ "$arg" == "-o" ]; then + arg_is_output_dir=yes + output_dir_set=yes + elif [ "$arg" == "-m" ]; then + arg_is_mode=yes + continue + elif [ -n "$arg_is_output_dir" ]; then + arg_is_output_dir= + output_dir="$arg" + elif [ -n "$arg_is_mode" ]; then + arg_is_mode= + continue + fi + + args+=( "$arg" ) + done + [ -n "$output_dir_set" ] || args=( -o "$output_dir" "${args[@]}" ) +} + + +exec_command () +{ + if [ -n "$dry_run" ]; then + echo "$@" + else + eval "$@" + fi +} + +set_args "$@" +basedir="$(dirname $0)" +exec_command "$basedir/run_tests.sh" -m create "${args[@]}" +exec_command "$basedir/manage_packages.sh" -d "$output_dir" +exec_command "$basedir/run_tests.sh" -m verify-install "${args[@]}" +exec_command "$basedir/manage_packages.sh" -d "$output_dir" -u +exec_command "$basedir/run_tests.sh" -m verify-uninstall "${args[@]}" diff --git a/test/jdk/tools/jpackage/windows/WinConsoleTest.java b/test/jdk/tools/jpackage/windows/WinConsoleTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/windows/WinConsoleTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import java.io.InputStream; +import java.io.FileInputStream; +import java.io.IOException; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.Annotations.Parameter; + +/* + * @test + * @summary jpackage with --win-console + * @library ../helpers + * @build jdk.jpackage.test.* + * @requires (os.family == "windows") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile WinConsoleTest.java + * + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-before-run=jdk.jpackage.test.JPackageCommand.useToolProviderByDefault + * --jpt-run=WinConsoleTest + * + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=WinConsoleTest + */ +public class WinConsoleTest { + + @Test + @Parameter("true") + @Parameter("false") + public static void test(boolean withWinConsole) throws IOException { + JPackageCommand cmd = JPackageCommand.helloAppImage(); + if (!withWinConsole) { + cmd.removeArgument("--win-console"); + } + cmd.executeAndAssertHelloAppImageCreated(); + checkSubsystem(cmd.appLauncherPath(), withWinConsole); + } + + private static void checkSubsystem(Path path, boolean isConsole) throws + IOException { + final int subsystemGui = 2; + final int subsystemConsole = 3; + final int bufferSize = 512; + + final int expectedSubsystem = isConsole ? subsystemConsole : subsystemGui; + + try (InputStream inputStream = new FileInputStream(path.toString())) { + byte[] bytes = new byte[bufferSize]; + TKit.assertEquals(bufferSize, inputStream.read(bytes), + String.format("Check %d bytes were read from %s file", + bufferSize, path)); + + // Check PE header for console or Win GUI app. + // https://docs.microsoft.com/en-us/windows/desktop/api/winnt/ns-winnt-_image_nt_headers + for (int i = 0; i < (bytes.length - 4); i++) { + if (bytes[i] == 0x50 && bytes[i + 1] == 0x45 + && bytes[i + 2] == 0x0 && bytes[i + 3] == 0x0) { + + // Signature, File Header and subsystem offset. + i = i + 4 + 20 + 68; + byte subsystem = bytes[i]; + TKit.assertEquals(expectedSubsystem, subsystem, + String.format("Check subsystem of PE [%s] file", + path)); + return; + } + } + } + + TKit.assertUnexpected(String.format( + "Subsystem not found in PE header of [%s] file", path)); + } +} diff --git a/test/jdk/tools/jpackage/windows/WinDirChooserTest.java b/test/jdk/tools/jpackage/windows/WinDirChooserTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/windows/WinDirChooserTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; + +/** + * Test --win-dir-chooser parameter. Output of the test should be + * WinDirChooserTest-1.0.exe installer. The output installer should provide the + * same functionality as the default installer (see description of the default + * installer in SimplePackageTest.java) plus provide an option for user to + * change the default installation directory. + */ + +/* + * @test + * @summary jpackage with --win-dir-chooser + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "windows") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @run main/othervm/timeout=360 -Xmx512m WinDirChooserTest + */ + +public class WinDirChooserTest { + public static void main(String[] args) { + TKit.run(args, () -> { + new PackageTest() + .forTypes(PackageType.WINDOWS) + .configureHelloApp() + .addInitializer(cmd -> cmd.addArgument("--win-dir-chooser")).run(); + }); + } +} diff --git a/test/jdk/tools/jpackage/windows/WinMenuGroupTest.java b/test/jdk/tools/jpackage/windows/WinMenuGroupTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/windows/WinMenuGroupTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.Annotations.Test; + +/** + * Test --win-menu and --win-menu-group parameters. + * Output of the test should be WinMenuGroupTest-1.0.exe installer. + * The output installer should provide the + * same functionality as the default installer (see description of the default + * installer in SimplePackageTest.java) plus + * it should create a shortcut for application launcher in Windows Menu in + * "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\WinMenuGroupTest_MenuGroup" folder. + */ + +/* + * @test + * @summary jpackage with --win-menu and --win-menu-group + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "windows") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile WinMenuGroupTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=WinMenuGroupTest + */ + +public class WinMenuGroupTest { + @Test + public static void test() { + new PackageTest() + .forTypes(PackageType.WINDOWS) + .configureHelloApp() + .addInitializer(cmd -> cmd.addArguments( + "--win-menu", "--win-menu-group", "WinMenuGroupTest_MenuGroup")) + .run(); + } +} diff --git a/test/jdk/tools/jpackage/windows/WinMenuTest.java b/test/jdk/tools/jpackage/windows/WinMenuTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/windows/WinMenuTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.Annotations.Test; + +/** + * Test --win-menu parameter. Output of the test should be WinMenuTest-1.0.exe + * installer. The output installer should provide the same functionality as the + * default installer (see description of the default installer in + * SimplePackageTest.java), except it should create a menu shortcut. + */ + +/* + * @test + * @summary jpackage with --win-menu + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "windows") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile WinMenuTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=WinMenuTest + */ + +public class WinMenuTest { + @Test + public static void test() { + new PackageTest() + .forTypes(PackageType.WINDOWS) + .configureHelloApp() + .addInitializer(cmd -> cmd.addArgument("--win-menu")).run(); + } +} diff --git a/test/jdk/tools/jpackage/windows/WinPerUserInstallTest.java b/test/jdk/tools/jpackage/windows/WinPerUserInstallTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/windows/WinPerUserInstallTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.Annotations.Test; + +/** + * Test --win-per-user-install, --win-menu, --win-menu-group parameters. + * Output of the test should be WinPerUserInstallTest-1.0.exe installer. The + * output installer should provide the same functionality as the default + * installer (see description of the default installer in + * SimplePackageTest.java) plus it should create application menu in Windows + * Menu and installation should be per user and not machine wide. + */ + +/* + * @test + * @summary jpackage with --win-per-user-install, --win-menu, --win-menu-group + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "windows") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile WinPerUserInstallTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=WinPerUserInstallTest + */ + +public class WinPerUserInstallTest { + @Test + public static void test() { + new PackageTest() + .forTypes(PackageType.WINDOWS) + .configureHelloApp() + .addInitializer(cmd -> cmd.addArguments( + "--win-menu", + "--win-menu-group", "WinPerUserInstallTest_MenuGroup", + "--win-per-user-install")) + .run(); + } +} diff --git a/test/jdk/tools/jpackage/windows/WinResourceTest.java b/test/jdk/tools/jpackage/windows/WinResourceTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/windows/WinResourceTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.nio.file.Path; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.Annotations.Parameters; +import java.util.List; + +/** + * Test --resource-dir option. The test should set --resource-dir to point to + * a dir with an empty "main.wxs" file. As a result, jpackage should try to + * use the customized resource and fail. + */ + +/* + * @test + * @summary jpackage with --resource-dir + * @library ../helpers + * @build jdk.jpackage.test.* + * @requires (os.family == "windows") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile WinResourceTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=WinResourceTest + */ + +public class WinResourceTest { + + public WinResourceTest(String wixSource, String expectedLogMessage) { + this.wixSource = wixSource; + this.expectedLogMessage = expectedLogMessage; + } + + @Parameters + public static List data() { + return List.of(new Object[][]{ + {"main.wxs", "Using custom package resource [Main WiX project file]"}, + {"overrides.wxi", "Using custom package resource [Overrides WiX project file]"}, + }); + } + + @Test + public void test() throws IOException { + new PackageTest() + .forTypes(PackageType.WINDOWS) + .configureHelloApp() + .addInitializer(cmd -> { + Path resourceDir = TKit.createTempDirectory("resources"); + + // 1. Set fake run time to save time by skipping jlink step of jpackage. + // 2. Instruct test to save jpackage output. + cmd.setFakeRuntime().saveConsoleOutput(true); + + cmd.addArguments("--resource-dir", resourceDir); + // Create invalid WiX source file in a resource dir. + TKit.createTextFile(resourceDir.resolve(wixSource), List.of( + "any string that is an invalid WiX source file")); + }) + .addBundleVerifier((cmd, result) -> { + // Assert jpackage picked custom main.wxs and failed as expected by + // examining its output + TKit.assertTextStream(expectedLogMessage) + .predicate(String::startsWith) + .apply(result.getOutput().stream()); + TKit.assertTextStream("error CNDL0104 : Not a valid source file") + .apply(result.getOutput().stream()); + }) + .setExpectedExitCode(1) + .run(); + } + + final String wixSource; + final String expectedLogMessage; +} diff --git a/test/jdk/tools/jpackage/windows/WinScriptTest.java b/test/jdk/tools/jpackage/windows/WinScriptTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/windows/WinScriptTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.ArrayList; +import jdk.incubator.jpackage.internal.IOUtils; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.Annotations.Parameter; +import jdk.jpackage.test.Annotations.Parameters; +import jdk.jpackage.test.JPackageCommand; + +/* + * @test usage of scripts from resource dir + * @summary jpackage with + * @library ../helpers + * @build jdk.jpackage.test.* + * @requires (os.family == "windows") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile WinScriptTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=WinScriptTest + */ + +public class WinScriptTest { + + public WinScriptTest(PackageType type) { + this.packageType = type; + + test = new PackageTest() + .forTypes(type) + .configureHelloApp() + .addInitializer(cmd -> { + cmd.setFakeRuntime().saveConsoleOutput(true); + }); + } + + @Parameters + public static List data() { + return List.of(new Object[][]{ + {PackageType.WIN_MSI}, + {PackageType.WIN_EXE} + }); + } + + @Test + @Parameter("0") + @Parameter("10") + public void test(int wsfExitCode) { + final ScriptData appImageScriptData; + if (wsfExitCode != 0 && packageType == PackageType.WIN_EXE) { + appImageScriptData = new ScriptData(PackageType.WIN_MSI, 0); + } else { + appImageScriptData = new ScriptData(PackageType.WIN_MSI, wsfExitCode); + } + + final ScriptData msiScriptData = new ScriptData(PackageType.WIN_EXE, wsfExitCode); + + test.setExpectedExitCode(wsfExitCode == 0 ? 0 : 1); + TKit.withTempDirectory("resources", tempDir -> { + test.addInitializer(cmd -> { + cmd.addArguments("--resource-dir", tempDir); + + appImageScriptData.createScript(cmd); + msiScriptData.createScript(cmd); + }); + + if (packageType == PackageType.WIN_MSI) { + test.addBundleVerifier((cmd, result) -> { + appImageScriptData.assertJPackageOutput(result.getOutput()); + }); + } + + if (packageType == PackageType.WIN_EXE) { + test.addBundleVerifier((cmd, result) -> { + appImageScriptData.assertJPackageOutput(result.getOutput()); + msiScriptData.assertJPackageOutput(result.getOutput()); + }); + } + + test.run(); + }); + } + + private static class ScriptData { + ScriptData(PackageType scriptType, int wsfExitCode) { + if (scriptType == PackageType.WIN_MSI) { + echoText = "post app image wsf"; + envVarName = "JpAppImageDir"; + scriptSuffixName = "post-image"; + } else { + echoText = "post msi wsf"; + envVarName = "JpMsiFile"; + scriptSuffixName = "post-msi"; + } + this.wsfExitCode = wsfExitCode; + } + + void assertJPackageOutput(List output) { + TKit.assertTextStream(String.format("jp: %s", echoText)) + .predicate(String::equals) + .apply(output.stream()); + + String cwdPattern = String.format("jp: CWD(%s)=", envVarName); + TKit.assertTextStream(cwdPattern) + .predicate(String::startsWith) + .apply(output.stream()); + String cwd = output.stream().filter(line -> line.startsWith( + cwdPattern)).findFirst().get().substring(cwdPattern.length()); + + String envVarPattern = String.format("jp: %s=", envVarName); + TKit.assertTextStream(envVarPattern) + .predicate(String::startsWith) + .apply(output.stream()); + String envVar = output.stream().filter(line -> line.startsWith( + envVarPattern)).findFirst().get().substring(envVarPattern.length()); + + TKit.assertTrue(envVar.startsWith(cwd), String.format( + "Check value of %s environment variable [%s] starts with the current directory [%s] set for %s script", + envVarName, envVar, cwd, echoText)); + } + + void createScript(JPackageCommand cmd) throws IOException { + IOUtils.createXml(Path.of(cmd.getArgumentValue("--resource-dir"), + String.format("%s-%s.wsf", cmd.name(), scriptSuffixName)), xml -> { + xml.writeStartElement("job"); + xml.writeAttribute("id", "main"); + xml.writeStartElement("script"); + xml.writeAttribute("language", "JScript"); + xml.writeCData(String.join("\n", List.of( + "var shell = new ActiveXObject('WScript.Shell')", + "WScript.Echo('jp: " + envVarName + "=' + shell.ExpandEnvironmentStrings('%" + envVarName + "%'))", + "WScript.Echo('jp: CWD(" + envVarName + ")=' + shell.CurrentDirectory)", + String.format("WScript.Echo('jp: %s')", echoText), + String.format("WScript.Quit(%d)", wsfExitCode) + ))); + xml.writeEndElement(); + xml.writeEndElement(); + }); + } + + private final int wsfExitCode; + private final String scriptSuffixName; + private final String echoText; + private final String envVarName; + } + + private final PackageType packageType; + private PackageTest test; +} diff --git a/test/jdk/tools/jpackage/windows/WinShortcutTest.java b/test/jdk/tools/jpackage/windows/WinShortcutTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/windows/WinShortcutTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.Annotations.Test; + +/** + * Test --win-shortcut parameter. Output of the test should be + * WinShortcutTest-1.0.exe installer. The output installer should provide the + * same functionality as the default installer (see description of the default + * installer in SimplePackageTest.java) plus install application shortcut on the + * desktop. + */ + +/* + * @test + * @summary jpackage with --win-shortcut + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "windows") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @compile WinShortcutTest.java + * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=WinShortcutTest + */ + +public class WinShortcutTest { + @Test + public static void test() { + new PackageTest() + .forTypes(PackageType.WINDOWS) + .configureHelloApp() + .addInitializer(cmd -> cmd.addArgument("--win-shortcut")) + .run(); + } +} diff --git a/test/jdk/tools/jpackage/windows/WinUpgradeUUIDTest.java b/test/jdk/tools/jpackage/windows/WinUpgradeUUIDTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/tools/jpackage/windows/WinUpgradeUUIDTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; + +/** + * Test both --win-upgrade-uuid and --app-version parameters. Output of the test + * should be WinUpgradeUUIDTest-1.0.exe and WinUpgradeUUIDTest-2.0.exe + * installers. Both output installers should provide the same functionality as + * the default installer (see description of the default installer in + * SimplePackageTest.java) but have the same product code and different + * versions. Running WinUpgradeUUIDTest-2.0.exe installer should automatically + * uninstall older version of the test application previously installed with + * WinUpgradeUUIDTest-1.0.exe installer. + */ + +/* + * @test + * @summary jpackage with --win-upgrade-uuid and --app-version + * @library ../helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @requires (os.family == "windows") + * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal + * @run main/othervm/timeout=360 -Xmx512m WinUpgradeUUIDTest + */ + +public class WinUpgradeUUIDTest { + public static void main(String[] args) { + TKit.run(args, () -> { + PackageTest test = init(); + if (test.getAction() != PackageTest.Action.VERIFY_INSTALL) { + test.run(); + } + + test = init(); + test.addInitializer(cmd -> { + cmd.setArgumentValue("--app-version", "2.0"); + cmd.setArgumentValue("--arguments", "bar"); + }); + test.run(); + }); + } + + private static PackageTest init() { + return new PackageTest() + .forTypes(PackageType.WINDOWS) + .configureHelloApp() + .addInitializer(cmd -> cmd.addArguments("--win-upgrade-uuid", + "F0B18E75-52AD-41A2-BC86-6BE4FCD50BEB")); + } +} diff --git a/test/jdk/tools/launcher/HelpFlagsTest.java b/test/jdk/tools/launcher/HelpFlagsTest.java --- a/test/jdk/tools/launcher/HelpFlagsTest.java +++ b/test/jdk/tools/launcher/HelpFlagsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -156,13 +156,12 @@ new ToolHelpSpec("jstatd", 1, 1, 1, 0, 0, 0, 1), // -?, -h, --help new ToolHelpSpec("keytool", 1, 1, 1, 0, 1, 0, 1), // none, prints help message anyways. new ToolHelpSpec("pack200", 1, 1, 1, 0, 1, 0, 2), // -?, -h, --help, -help accepted but not documented. - new ToolHelpSpec("rmic", 0, 0, 0, 0, 0, 0, 1), // none, pirnts help message anyways. + new ToolHelpSpec("rmic", 0, 0, 0, 0, 0, 0, 1), // none, prints help message anyways. new ToolHelpSpec("rmid", 0, 0, 0, 0, 0, 0, 1), // none, prints help message anyways. new ToolHelpSpec("rmiregistry", 0, 0, 0, 0, 0, 0, 1), // none, prints help message anyways. new ToolHelpSpec("serialver", 0, 0, 0, 0, 0, 0, 1), // none, prints help message anyways. new ToolHelpSpec("unpack200", 1, 1, 1, 0, 1, 0, 2), // -?, -h, --help, -help accepted but not documented. - // Oracle proprietary tools: - new ToolHelpSpec("javapackager",0, 0, 0, 0, 1, 0, 255), // -help accepted but not documented. + new ToolHelpSpec("jpackage", 0, 1, 1, 0, 0, 1, 1), // -h, --help, }; // Returns true if the file is not a tool. diff --git a/test/jdk/tools/launcher/VersionCheck.java b/test/jdk/tools/launcher/VersionCheck.java --- a/test/jdk/tools/launcher/VersionCheck.java +++ b/test/jdk/tools/launcher/VersionCheck.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -62,7 +62,7 @@ "jmc", "jmc.ini", "jweblauncher", - "packager", + "jpackage", "ssvagent", "unpack200", }; @@ -108,7 +108,7 @@ "klist", "ktab", "pack200", - "packager", + "jpackage", "rmic", "rmid", "rmiregistry",