--- old/make/CompileJavaModules.gmk 2019-12-03 13:25:32.414252000 -0500 +++ new/make/CompileJavaModules.gmk 2019-12-03 13:25:30.679244500 -0500 @@ -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 \ --- old/make/common/Modules.gmk 2019-12-03 13:25:46.862472400 -0500 +++ new/make/common/Modules.gmk 2019-12-03 13:25:45.477675900 -0500 @@ -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 --- old/make/common/NativeCompilation.gmk 2019-12-03 13:26:00.850796200 -0500 +++ new/make/common/NativeCompilation.gmk 2019-12-03 13:25:59.468980400 -0500 @@ -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) \ --- old/src/java.base/share/classes/module-info.java 2019-12-03 13:26:14.948922200 -0500 +++ new/src/java.base/share/classes/module-info.java 2019-12-03 13:26:13.569983400 -0500 @@ -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, --- old/test/jdk/tools/launcher/HelpFlagsTest.java 2019-12-03 13:26:29.378952900 -0500 +++ new/test/jdk/tools/launcher/HelpFlagsTest.java 2019-12-03 13:26:28.018822200 -0500 @@ -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. --- old/test/jdk/tools/launcher/VersionCheck.java 2019-12-03 13:26:43.271492900 -0500 +++ new/test/jdk/tools/launcher/VersionCheck.java 2019-12-03 13:26:41.962572300 -0500 @@ -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", --- /dev/null 2019-12-03 13:26:57.000000000 -0500 +++ new/make/launcher/Launcher-jdk.incubator.jpackage.gmk 2019-12-03 13:26:55.542856300 -0500 @@ -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, \ +)) --- /dev/null 2019-12-03 13:27:06.000000000 -0500 +++ new/make/lib/Lib-jdk.incubator.jpackage.gmk 2019-12-03 13:27:03.669406700 -0500 @@ -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 --- /dev/null 2019-12-03 13:27:14.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/DesktopIntegration.java 2019-12-03 13:27:12.041657200 -0500 @@ -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) + ); +} --- /dev/null 2019-12-03 13:27:22.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LibProvidersLookup.java 2019-12-03 13:27:20.132933800 -0500 @@ -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}+\\)"); +} --- /dev/null 2019-12-03 13:27:30.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppBundler.java 2019-12-03 13:27:28.169672100 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:27:38.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppImageBuilder.java 2019-12-03 13:27:36.197368400 -0500 @@ -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); + } + } + } + +} --- /dev/null 2019-12-03 13:27:46.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxDebBundler.java 2019-12-03 13:27:44.204440500 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 13:27:54.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxPackageBundler.java 2019-12-03 13:27:52.477020100 -0500 @@ -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 + ); + +} --- /dev/null 2019-12-03 13:28:03.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxRpmBundler.java 2019-12-03 13:28:00.691762600 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:28:11.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/PackageProperty.java 2019-12-03 13:28:08.843848300 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:28:19.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources.properties 2019-12-03 13:28:16.840067400 -0500 @@ -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 --- /dev/null 2019-12-03 13:28:27.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources_ja.properties 2019-12-03 13:28:24.972966400 -0500 @@ -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 --- /dev/null 2019-12-03 13:28:35.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources_zh_CN.properties 2019-12-03 13:28:32.961214700 -0500 @@ -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 Binary files /dev/null and new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/java32.png differ --- /dev/null 2019-12-03 13:28:51.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.control 2019-12-03 13:28:49.440629000 -0500 @@ -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 --- /dev/null 2019-12-03 13:28:59.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.copyright 2019-12-03 13:28:57.462182000 -0500 @@ -0,0 +1,5 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: * +Copyright: APPLICATION_COPYRIGHT +License: APPLICATION_LICENSE_TEXT --- /dev/null 2019-12-03 13:29:07.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.desktop 2019-12-03 13:29:05.426754700 -0500 @@ -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 --- /dev/null 2019-12-03 13:29:16.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.postinst 2019-12-03 13:29:14.132782800 -0500 @@ -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 --- /dev/null 2019-12-03 13:29:24.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.postrm 2019-12-03 13:29:22.261967800 -0500 @@ -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 --- /dev/null 2019-12-03 13:29:32.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.preinst 2019-12-03 13:29:30.320753200 -0500 @@ -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 --- /dev/null 2019-12-03 13:29:40.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.prerm 2019-12-03 13:29:38.533367100 -0500 @@ -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 + --- /dev/null 2019-12-03 13:29:49.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.spec 2019-12-03 13:29:46.635077400 -0500 @@ -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 --- /dev/null 2019-12-03 13:29:57.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/utils.sh 2019-12-03 13:29:54.644461000 -0500 @@ -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 "$@" +} --- /dev/null 2019-12-03 13:30:05.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/classes/module-info.java.extra 2019-12-03 13:30:02.681826600 -0500 @@ -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; + --- /dev/null 2019-12-03 13:30:13.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/native/jpackageapplauncher/launcher.cpp 2019-12-03 13:30:10.851409900 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:30:21.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/native/libapplauncher/LinuxPlatform.cpp 2019-12-03 13:30:19.214233500 -0500 @@ -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); +} --- /dev/null 2019-12-03 13:30:29.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/native/libapplauncher/LinuxPlatform.h 2019-12-03 13:30:27.264167500 -0500 @@ -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 --- /dev/null 2019-12-03 13:30:37.000000000 -0500 +++ new/src/jdk.incubator.jpackage/linux/native/libapplauncher/PlatformDefs.h 2019-12-03 13:30:35.301383100 -0500 @@ -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 --- /dev/null 2019-12-03 13:30:45.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/EnumeratedBundlerParam.java 2019-12-03 13:30:43.430720700 -0500 @@ -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(); + } + +} --- /dev/null 2019-12-03 13:30:54.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppBundler.java 2019-12-03 13:30:51.813251400 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:31:02.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppImageBuilder.java 2019-12-03 13:31:00.071718800 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:31:10.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppStoreBundler.java 2019-12-03 13:31:08.233343600 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:31:18.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacBaseInstallerBundler.java 2019-12-03 13:31:16.222893800 -0500 @@ -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; + } + } +} --- /dev/null 2019-12-03 13:31:27.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacCertificate.java 2019-12-03 13:31:24.894182800 -0500 @@ -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; + } +} --- /dev/null 2019-12-03 13:31:35.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacDmgBundler.java 2019-12-03 13:31:32.857802200 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:31:43.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacPkgBundler.java 2019-12-03 13:31:40.867706500 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:31:51.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/DMGsetup.scpt 2019-12-03 13:31:48.846908500 -0500 @@ -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 --- /dev/null 2019-12-03 13:31:59.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/Info-lite.plist.template 2019-12-03 13:31:56.753708600 -0500 @@ -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 + + --- /dev/null 2019-12-03 13:32:07.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacAppStore.entitlements 2019-12-03 13:32:05.162894700 -0500 @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + --- /dev/null 2019-12-03 13:32:16.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacAppStore_Inherit.entitlements 2019-12-03 13:32:14.007161000 -0500 @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.inherit + + + --- /dev/null 2019-12-03 13:32:24.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources.properties 2019-12-03 13:32:22.310367700 -0500 @@ -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. --- /dev/null 2019-12-03 13:32:33.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources_ja.properties 2019-12-03 13:32:30.869025900 -0500 @@ -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. --- /dev/null 2019-12-03 13:32:46.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources_zh_CN.properties 2019-12-03 13:32:39.251129200 -0500 @@ -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. --- /dev/null 2019-12-03 13:32:55.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/Runtime-Info.plist.template 2019-12-03 13:32:53.290692000 -0500 @@ -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 + + Binary files /dev/null and new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/background_dmg.png differ Binary files /dev/null and new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/background_pkg.png differ Binary files /dev/null and new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/java.icns differ --- /dev/null 2019-12-03 13:33:28.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/lic_template.plist 2019-12-03 13:33:25.657825800 -0500 @@ -0,0 +1,244 @@ + + + + + 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 + + + + --- /dev/null 2019-12-03 13:33:36.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/postinstall.template 2019-12-03 13:33:33.677480400 -0500 @@ -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 --- /dev/null 2019-12-03 13:33:44.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/preinstall.template 2019-12-03 13:33:41.664516600 -0500 @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +if [ ! -d "INSTALL_LOCATION" ] +then + mkdir -p "INSTALL_LOCATION" +fi + +exit 0 --- /dev/null 2019-12-03 13:33:52.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/classes/module-info.java.extra 2019-12-03 13:33:49.679978600 -0500 @@ -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; + --- /dev/null 2019-12-03 13:34:00.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/native/jpackageapplauncher/main.m 2019-12-03 13:33:57.981559300 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:34:08.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/native/libapplauncher/MacPlatform.h 2019-12-03 13:34:05.978517500 -0500 @@ -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 --- /dev/null 2019-12-03 13:34:16.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/native/libapplauncher/MacPlatform.mm 2019-12-03 13:34:14.068952200 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:34:24.000000000 -0500 +++ new/src/jdk.incubator.jpackage/macosx/native/libapplauncher/PlatformDefs.h 2019-12-03 13:34:22.040916500 -0500 @@ -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 --- /dev/null 2019-12-03 13:34:32.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractAppImageBuilder.java 2019-12-03 13:34:30.481133900 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 13:34:40.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractBundler.java 2019-12-03 13:34:38.543316200 -0500 @@ -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()); + } + } +} --- /dev/null 2019-12-03 13:34:48.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AbstractImageBundler.java 2019-12-03 13:34:46.472795600 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:34:57.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AddLauncherArguments.java 2019-12-03 13:34:55.149721100 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:35:05.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/AppImageFile.java 2019-12-03 13:35:03.284125700 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:35:13.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ApplicationLayout.java 2019-12-03 13:35:11.331260800 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:35:22.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ArgAction.java 2019-12-03 13:35:19.659400000 -0500 @@ -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(); +} --- /dev/null 2019-12-03 13:35:31.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Arguments.java 2019-12-03 13:35:28.464289000 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:35:39.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/BasicBundlers.java 2019-12-03 13:35:37.128385500 -0500 @@ -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); + } + } +} --- /dev/null 2019-12-03 13:35:48.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/BundleParams.java 2019-12-03 13:35:45.953425300 -0500 @@ -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); + } + } +} --- /dev/null 2019-12-03 13:35:56.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Bundler.java 2019-12-03 13:35:54.155089600 -0500 @@ -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(); +} --- /dev/null 2019-12-03 13:36:06.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/BundlerParamInfo.java 2019-12-03 13:36:03.225648700 -0500 @@ -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; + } +} --- /dev/null 2019-12-03 13:36:14.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Bundlers.java 2019-12-03 13:36:12.275705200 -0500 @@ -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); + +} --- /dev/null 2019-12-03 13:36:22.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/CLIHelp.java 2019-12-03 13:36:20.325174800 -0500 @@ -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)); + } + } +} --- /dev/null 2019-12-03 13:36:31.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ConfigException.java 2019-12-03 13:36:28.688073500 -0500 @@ -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; + } +} --- /dev/null 2019-12-03 13:36:39.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/DeployParams.java 2019-12-03 13:36:36.752675000 -0500 @@ -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 + "}}"; + } + +} --- /dev/null 2019-12-03 13:36:47.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/DottedVersion.java 2019-12-03 13:36:44.883149800 -0500 @@ -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+"); +} --- /dev/null 2019-12-03 13:36:55.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Executor.java 2019-12-03 13:36:52.991238000 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:37:03.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/FileAssociation.java 2019-12-03 13:37:01.251941200 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:37:12.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/I18N.java 2019-12-03 13:37:09.583004700 -0500 @@ -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"); + } + } +} --- /dev/null 2019-12-03 13:37:20.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/IOUtils.java 2019-12-03 13:37:17.753796000 -0500 @@ -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"; + } +} --- /dev/null 2019-12-03 13:37:29.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/JLinkBundlerHelper.java 2019-12-03 13:37:27.003870000 -0500 @@ -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); + } +} --- /dev/null 2019-12-03 13:37:39.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/JPackageToolProvider.java 2019-12-03 13:37:36.557594200 -0500 @@ -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; + } + } +} --- /dev/null 2019-12-03 13:37:48.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Log.java 2019-12-03 13:37:45.793956100 -0500 @@ -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); + } + } +} --- /dev/null 2019-12-03 13:37:56.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ModFile.java 2019-12-03 13:37:53.937436200 -0500 @@ -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("[.][^.]+$", ""); + } +} --- /dev/null 2019-12-03 13:38:05.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/OverridableResource.java 2019-12-03 13:38:02.272618600 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:38:14.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/PackagerException.java 2019-12-03 13:38:11.418678100 -0500 @@ -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); + } + +} --- /dev/null 2019-12-03 13:38:23.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/PathGroup.java 2019-12-03 13:38:20.833583200 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:38:31.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/Platform.java 2019-12-03 13:38:28.643810800 -0500 @@ -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"); + } +} --- /dev/null 2019-12-03 13:38:39.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/PlatformPackage.java 2019-12-03 13:38:36.623058000 -0500 @@ -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(); +} --- /dev/null 2019-12-03 13:38:46.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/RelativeFileSet.java 2019-12-03 13:38:44.557649000 -0500 @@ -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 + "}"; + } + +} --- /dev/null 2019-12-03 13:38:54.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ScriptRunner.java 2019-12-03 13:38:52.507165700 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:39:02.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/StandardBundlerParam.java 2019-12-03 13:39:00.506236000 -0500 @@ -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; + } +} --- /dev/null 2019-12-03 13:39:10.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ToolValidator.java 2019-12-03 13:39:08.489932500 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:39:18.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/ValidOptions.java 2019-12-03 13:39:16.436756000 -0500 @@ -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; + } +} --- /dev/null 2019-12-03 13:39:26.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/HelpResources.properties 2019-12-03 13:39:24.410108400 -0500 @@ -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\ + --- /dev/null 2019-12-03 13:39:34.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/HelpResources_ja.properties 2019-12-03 13:39:32.405232300 -0500 @@ -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\ + --- /dev/null 2019-12-03 13:39:42.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/HelpResources_zh_CN.properties 2019-12-03 13:39:40.342840000 -0500 @@ -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\ + --- /dev/null 2019-12-03 13:39:50.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/MainResources.properties 2019-12-03 13:39:48.196901900 -0500 @@ -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} --- /dev/null 2019-12-03 13:39:58.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/MainResources_ja.properties 2019-12-03 13:39:56.286509700 -0500 @@ -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} --- /dev/null 2019-12-03 13:40:06.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/MainResources_zh_CN.properties 2019-12-03 13:40:04.356421100 -0500 @@ -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} --- /dev/null 2019-12-03 13:40:14.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/resources/ResourceLocator.java 2019-12-03 13:40:12.278569700 -0500 @@ -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() { + } +} --- /dev/null 2019-12-03 13:40:22.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/main/CommandLine.java 2019-12-03 13:40:20.200710400 -0500 @@ -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(); + } + } + } +} --- /dev/null 2019-12-03 13:40:30.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/main/Main.java 2019-12-03 13:40:28.324028600 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:40:38.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/classes/module-info.java 2019-12-03 13:40:36.168270100 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:40:46.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/FileAttributes.h 2019-12-03 13:40:44.068685300 -0500 @@ -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 + --- /dev/null 2019-12-03 13:40:54.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/FilePath.h 2019-12-03 13:40:52.577680600 -0500 @@ -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 --- /dev/null 2019-12-03 13:41:03.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Helpers.cpp 2019-12-03 13:41:00.618439300 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:41:11.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Helpers.h 2019-12-03 13:41:08.682939800 -0500 @@ -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 --- /dev/null 2019-12-03 13:41:19.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/IniFile.cpp 2019-12-03 13:41:16.738960200 -0500 @@ -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(); +} --- /dev/null 2019-12-03 13:41:27.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/IniFile.h 2019-12-03 13:41:24.774866700 -0500 @@ -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 --- /dev/null 2019-12-03 13:41:35.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/JavaVirtualMachine.cpp 2019-12-03 13:41:32.781182200 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:41:43.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/JavaVirtualMachine.h 2019-12-03 13:41:40.866390200 -0500 @@ -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 --- /dev/null 2019-12-03 13:41:51.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Library.cpp 2019-12-03 13:41:49.044940400 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:41:59.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Library.h 2019-12-03 13:41:57.096635500 -0500 @@ -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 + --- /dev/null 2019-12-03 13:42:07.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Macros.cpp 2019-12-03 13:42:05.164790900 -0500 @@ -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)); +} --- /dev/null 2019-12-03 13:42:15.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Macros.h 2019-12-03 13:42:13.260342400 -0500 @@ -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 --- /dev/null 2019-12-03 13:42:23.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Messages.cpp 2019-12-03 13:42:21.165438400 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:42:31.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Messages.h 2019-12-03 13:42:29.174430700 -0500 @@ -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 --- /dev/null 2019-12-03 13:42:39.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/OrderedMap.h 2019-12-03 13:42:37.113878800 -0500 @@ -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 --- /dev/null 2019-12-03 13:42:47.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Package.cpp 2019-12-03 13:42:45.157800400 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:42:55.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Package.h 2019-12-03 13:42:53.250627400 -0500 @@ -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 --- /dev/null 2019-12-03 13:43:03.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Platform.cpp 2019-12-03 13:43:01.467515100 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:43:11.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Platform.h 2019-12-03 13:43:09.443404500 -0500 @@ -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 --- /dev/null 2019-12-03 13:43:19.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/PlatformString.cpp 2019-12-03 13:43:17.368033200 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:43:27.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/PlatformString.h 2019-12-03 13:43:25.418511400 -0500 @@ -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 --- /dev/null 2019-12-03 13:43:35.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/Properties.h 2019-12-03 13:43:33.266338200 -0500 @@ -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 + --- /dev/null 2019-12-03 13:43:43.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/PropertyFile.cpp 2019-12-03 13:43:41.274509700 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:43:51.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/PropertyFile.h 2019-12-03 13:43:49.246881400 -0500 @@ -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 --- /dev/null 2019-12-03 13:43:59.000000000 -0500 +++ new/src/jdk.incubator.jpackage/share/native/libapplauncher/main.cpp 2019-12-03 13:43:57.237828800 -0500 @@ -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() { + } +} --- /dev/null 2019-12-03 13:44:07.000000000 -0500 +++ new/src/jdk.incubator.jpackage/unix/native/libapplauncher/FileAttribute.h 2019-12-03 13:44:05.409423500 -0500 @@ -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 --- /dev/null 2019-12-03 13:44:15.000000000 -0500 +++ new/src/jdk.incubator.jpackage/unix/native/libapplauncher/FileAttributes.cpp 2019-12-03 13:44:13.322597300 -0500 @@ -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(); + } + } +} --- /dev/null 2019-12-03 13:44:23.000000000 -0500 +++ new/src/jdk.incubator.jpackage/unix/native/libapplauncher/FilePath.cpp 2019-12-03 13:44:21.495097500 -0500 @@ -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); +} --- /dev/null 2019-12-03 13:44:31.000000000 -0500 +++ new/src/jdk.incubator.jpackage/unix/native/libapplauncher/PosixPlatform.cpp 2019-12-03 13:44:29.493210400 -0500 @@ -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(); +} --- /dev/null 2019-12-03 13:44:39.000000000 -0500 +++ new/src/jdk.incubator.jpackage/unix/native/libapplauncher/PosixPlatform.h 2019-12-03 13:44:37.516769300 -0500 @@ -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 --- /dev/null 2019-12-03 13:44:47.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinAppBundler.java 2019-12-03 13:44:45.495423700 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:44:55.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinExeBundler.java 2019-12-03 13:44:53.479419200 -0500 @@ -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); +} --- /dev/null 2019-12-03 13:45:03.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WinMsiBundler.java 2019-12-03 13:45:01.424382900 -0500 @@ -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(); + +} --- /dev/null 2019-12-03 13:45:12.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsAppImageBuilder.java 2019-12-03 13:45:09.523519800 -0500 @@ -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); + +} --- /dev/null 2019-12-03 13:45:20.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsBundlerParam.java 2019-12-03 13:45:17.766973800 -0500 @@ -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 + ); +} --- /dev/null 2019-12-03 13:45:28.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsDefender.java 2019-12-03 13:45:25.920060100 -0500 @@ -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; + } +} --- /dev/null 2019-12-03 13:45:36.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WindowsRegistry.java 2019-12-03 13:45:33.933121200 -0500 @@ -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); +} --- /dev/null 2019-12-03 13:45:44.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixPipeline.java 2019-12-03 13:45:41.820663000 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:45:52.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixSourcesBuilder.java 2019-12-03 13:45:49.885252500 -0500 @@ -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) + ); +} --- /dev/null 2019-12-03 13:46:00.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixTool.java 2019-12-03 13:45:57.877862500 -0500 @@ -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()); + } +} --- /dev/null 2019-12-03 13:46:08.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/MsiInstallerStrings_en.wxl 2019-12-03 13:46:06.567032500 -0500 @@ -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. + --- /dev/null 2019-12-03 13:46:16.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/MsiInstallerStrings_ja.wxl 2019-12-03 13:46:14.424964900 -0500 @@ -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. + --- /dev/null 2019-12-03 13:46:24.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/MsiInstallerStrings_zh_CN.wxl 2019-12-03 13:46:22.506775000 -0500 @@ -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. + --- /dev/null 2019-12-03 13:46:32.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinLauncher.template 2019-12-03 13:46:30.523811600 -0500 @@ -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 --- /dev/null 2019-12-03 13:46:40.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources.properties 2019-12-03 13:46:38.533647900 -0500 @@ -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}". + --- /dev/null 2019-12-03 13:46:48.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_ja.properties 2019-12-03 13:46:46.478526200 -0500 @@ -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}". + --- /dev/null 2019-12-03 13:46:56.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/WinResources_zh_CN.properties 2019-12-03 13:46:54.447265700 -0500 @@ -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}". + Binary files /dev/null and new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/java48.ico differ --- /dev/null 2019-12-03 13:47:13.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/main.wxs 2019-12-03 13:47:10.568917600 -0500 @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + 1 + + + !(loc.message.install.dir.exist) + + + + + + + + 1 + INSTALLDIR_VALID="0" + INSTALLDIR_VALID="1" + + + + 1 + 1 + + + + + + + + + + + + + + + + + --- /dev/null 2019-12-03 13:47:20.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/resources/overrides.wxi 2019-12-03 13:47:18.607750100 -0500 @@ -0,0 +1,3 @@ + + + \ No newline at end of file --- /dev/null 2019-12-03 13:47:29.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/module-info.java.extra 2019-12-03 13:47:26.673473900 -0500 @@ -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; + --- /dev/null 2019-12-03 13:47:37.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/jpackageapplauncher/WinLauncher.cpp 2019-12-03 13:47:34.702771300 -0500 @@ -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; +} + --- /dev/null 2019-12-03 13:47:45.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libapplauncher/DllMain.cpp 2019-12-03 13:47:42.712697300 -0500 @@ -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; + } +} + --- /dev/null 2019-12-03 13:47:52.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libapplauncher/FileAttribute.h 2019-12-03 13:47:50.531800500 -0500 @@ -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 + --- /dev/null 2019-12-03 13:48:00.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libapplauncher/FilePath.cpp 2019-12-03 13:47:58.540587600 -0500 @@ -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(); + } + } +} --- /dev/null 2019-12-03 13:48:08.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libapplauncher/PlatformDefs.h 2019-12-03 13:48:06.467659600 -0500 @@ -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 --- /dev/null 2019-12-03 13:48:16.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libapplauncher/WindowsPlatform.cpp 2019-12-03 13:48:14.164458900 -0500 @@ -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(); +} --- /dev/null 2019-12-03 13:48:24.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libapplauncher/WindowsPlatform.h 2019-12-03 13:48:22.142120100 -0500 @@ -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 --- /dev/null 2019-12-03 13:48:32.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/ByteBuffer.cpp 2019-12-03 13:48:29.988465800 -0500 @@ -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); + } +} --- /dev/null 2019-12-03 13:48:40.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/ByteBuffer.h 2019-12-03 13:48:37.971699600 -0500 @@ -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 + --- /dev/null 2019-12-03 13:48:48.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/ErrorHandling.cpp 2019-12-03 13:48:45.922078300 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:48:56.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/ErrorHandling.h 2019-12-03 13:48:53.988784200 -0500 @@ -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 --- /dev/null 2019-12-03 13:49:04.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/FileUtils.cpp 2019-12-03 13:49:01.766678500 -0500 @@ -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 --- /dev/null 2019-12-03 13:49:12.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/FileUtils.h 2019-12-03 13:49:09.710636100 -0500 @@ -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 --- /dev/null 2019-12-03 13:49:20.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/IconSwap.cpp 2019-12-03 13:49:17.838831500 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:49:28.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/IconSwap.h 2019-12-03 13:49:25.717480700 -0500 @@ -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 + --- /dev/null 2019-12-03 13:49:36.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/Log.cpp 2019-12-03 13:49:33.791418500 -0500 @@ -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 << ")"); + } +} --- /dev/null 2019-12-03 13:49:44.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/Log.h 2019-12-03 13:49:41.831142700 -0500 @@ -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_ --- /dev/null 2019-12-03 13:49:52.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/ResourceEditor.cpp 2019-12-03 13:49:49.771372700 -0500 @@ -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); +} --- /dev/null 2019-12-03 13:50:00.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/ResourceEditor.h 2019-12-03 13:49:57.637889500 -0500 @@ -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 --- /dev/null 2019-12-03 13:50:07.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/SourceCodePos.h 2019-12-03 13:50:05.432971200 -0500 @@ -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 --- /dev/null 2019-12-03 13:50:15.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/SysInfo.h 2019-12-03 13:50:13.390754900 -0500 @@ -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 --- /dev/null 2019-12-03 13:50:23.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/UniqueHandle.h 2019-12-03 13:50:21.364674000 -0500 @@ -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 --- /dev/null 2019-12-03 13:50:31.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/Utils.cpp 2019-12-03 13:50:29.405521600 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:50:39.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/Utils.h 2019-12-03 13:50:37.415155400 -0500 @@ -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 --- /dev/null 2019-12-03 13:50:47.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/VersionInfoSwap.cpp 2019-12-03 13:50:45.379770700 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:50:55.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/VersionInfoSwap.h 2019-12-03 13:50:53.337491400 -0500 @@ -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 + --- /dev/null 2019-12-03 13:51:03.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/WinErrorHandling.cpp 2019-12-03 13:51:01.299066300 -0500 @@ -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)); +} --- /dev/null 2019-12-03 13:51:11.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/WinErrorHandling.h 2019-12-03 13:51:09.261790800 -0500 @@ -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 --- /dev/null 2019-12-03 13:51:19.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/WinSysInfo.cpp 2019-12-03 13:51:17.175263500 -0500 @@ -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 --- /dev/null 2019-12-03 13:51:28.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/WinSysInfo.h 2019-12-03 13:51:25.701691300 -0500 @@ -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 --- /dev/null 2019-12-03 13:51:35.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/WindowsRegistry.cpp 2019-12-03 13:51:33.615723000 -0500 @@ -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 --- /dev/null 2019-12-03 13:51:44.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/jpackage.cpp 2019-12-03 13:51:42.302391100 -0500 @@ -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 --- /dev/null 2019-12-03 13:51:52.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/tstrings.cpp 2019-12-03 13:51:50.410963200 -0500 @@ -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 --- /dev/null 2019-12-03 13:52:01.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libjpackage/tstrings.h 2019-12-03 13:51:59.099415900 -0500 @@ -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 --- /dev/null 2019-12-03 13:52:09.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/libwixhelper/libwixhelper.cpp 2019-12-03 13:52:07.335311900 -0500 @@ -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; + } +} --- /dev/null 2019-12-03 13:52:18.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/msiwrapper/Executor.cpp 2019-12-03 13:52:16.017699900 -0500 @@ -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); +} --- /dev/null 2019-12-03 13:52:26.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/msiwrapper/Executor.h 2019-12-03 13:52:24.304461800 -0500 @@ -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 --- /dev/null 2019-12-03 13:52:34.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/msiwrapper/MsiWrapper.cpp 2019-12-03 13:52:32.222618400 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:52:42.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/msiwrapper/Resources.cpp 2019-12-03 13:52:40.198217000 -0500 @@ -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); +} --- /dev/null 2019-12-03 13:52:50.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/native/msiwrapper/Resources.h 2019-12-03 13:52:47.960697100 -0500 @@ -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 --- /dev/null 2019-12-03 13:52:58.000000000 -0500 +++ new/test/jdk/tools/jpackage/TEST.properties 2019-12-03 13:52:56.148957400 -0500 @@ -0,0 +1,3 @@ +keys=jpackagePlatformPackage +requires.properties=jpackage.test.SQETest +maxOutputSize=2000000 --- /dev/null 2019-12-03 13:53:06.000000000 -0500 +++ new/test/jdk/tools/jpackage/apps/com.hello/com/hello/Hello.java 2019-12-03 13:53:04.549975900 -0500 @@ -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()); + } + } + +} --- /dev/null 2019-12-03 13:53:15.000000000 -0500 +++ new/test/jdk/tools/jpackage/apps/com.hello/module-info.java 2019-12-03 13:53:12.587031000 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:53:22.000000000 -0500 +++ new/test/jdk/tools/jpackage/apps/com.other/com/other/Other.java 2019-12-03 13:53:20.561889100 -0500 @@ -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()); + } + } + +} --- /dev/null 2019-12-03 13:53:30.000000000 -0500 +++ new/test/jdk/tools/jpackage/apps/com.other/module-info.java 2019-12-03 13:53:28.452007100 -0500 @@ -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; +} Binary files /dev/null and new/test/jdk/tools/jpackage/apps/dukeplug.png differ --- /dev/null 2019-12-03 13:53:46.000000000 -0500 +++ new/test/jdk/tools/jpackage/apps/image/Hello.java 2019-12-03 13:53:44.196776700 -0500 @@ -0,0 +1,177 @@ +/* + * 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.awt.AWTError; +import java.awt.Desktop; +import java.awt.GraphicsEnvironment; +import java.awt.desktop.OpenFilesEvent; +import java.awt.desktop.OpenFilesHandler; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Path; +import java.nio.file.Files; +import java.util.stream.Collectors; +import java.util.List; +import java.util.ArrayList; +import java.util.stream.Stream; +import java.util.Collections; + +public class Hello implements OpenFilesHandler { + + public static void main(String[] args) throws IOException, InterruptedException { + var faFiles = getFaFiles(); + if (faFiles != null) { + // Some files got opened through fa mechanizm. + // They are the arguments then. + args = faFiles.toArray(String[]::new); + } + + var lines = printArgs(args); + + lines.forEach(System.out::println); + + var outputFile = getOutputFile(args); + trace(String.format("Output file: [%s]", outputFile)); + Files.write(outputFile, lines); + } + + private static List 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); + } +} --- /dev/null 2019-12-03 13:53:54.000000000 -0500 +++ new/test/jdk/tools/jpackage/apps/installer/Hello.java 2019-12-03 13:53:51.976628700 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 13:54:02.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/JPackageHelper.java 2019-12-03 13:54:00.167138800 -0500 @@ -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; + } +} --- /dev/null 2019-12-03 13:54:10.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/JPackageInstallerHelper.java 2019-12-03 13:54:08.226210600 -0500 @@ -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()); + } + } + } +} --- /dev/null 2019-12-03 13:54:18.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/JPackagePath.java 2019-12-03 13:54:16.019346000 -0500 @@ -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; + } +} --- /dev/null 2019-12-03 13:54:26.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Annotations.java 2019-12-03 13:54:24.054555000 -0500 @@ -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 { + } +} --- /dev/null 2019-12-03 13:54:34.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java 2019-12-03 13:54:32.264258100 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:54:43.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CommandArguments.java 2019-12-03 13:54:40.653116600 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:54:52.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java 2019-12-03 13:54:50.137532300 -0500 @@ -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 + }; +} --- /dev/null 2019-12-03 13:55:00.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java 2019-12-03 13:54:58.129475100 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:55:08.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Functional.java 2019-12-03 13:55:06.157393000 -0500 @@ -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); + } +} --- /dev/null 2019-12-03 13:55:16.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java 2019-12-03 13:55:14.087398500 -0500 @@ -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]; +} --- /dev/null 2019-12-03 13:55:25.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java 2019-12-03 13:55:22.524325400 -0500 @@ -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(); +} --- /dev/null 2019-12-03 13:55:33.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java 2019-12-03 13:55:30.752764000 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:55:41.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaAppDesc.java 2019-12-03 13:55:38.833689800 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:55:49.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaTool.java 2019-12-03 13:55:46.842584100 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:55:57.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java 2019-12-03 13:55:54.718089800 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:56:05.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java 2019-12-03 13:56:02.813809000 -0500 @@ -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")); + +} --- /dev/null 2019-12-03 13:56:13.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Main.java 2019-12-03 13:56:10.595398900 -0500 @@ -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; + } +} --- /dev/null 2019-12-03 13:56:20.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MethodCall.java 2019-12-03 13:56:18.508149900 -0500 @@ -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]; +} --- /dev/null 2019-12-03 13:56:28.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java 2019-12-03 13:56:26.569282400 -0500 @@ -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))); + } +} --- /dev/null 2019-12-03 13:56:36.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageType.java 2019-12-03 13:56:34.503294900 -0500 @@ -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()); + } +} --- /dev/null 2019-12-03 13:56:44.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java 2019-12-03 13:56:42.534060900 -0500 @@ -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")); + } + } +} --- /dev/null 2019-12-03 13:56:52.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java 2019-12-03 13:56:50.422251300 -0500 @@ -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-"; +} --- /dev/null 2019-12-03 13:57:00.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java 2019-12-03 13:56:58.446411000 -0500 @@ -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(); + +} --- /dev/null 2019-12-03 13:57:08.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java 2019-12-03 13:57:06.376925900 -0500 @@ -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<>(); +} --- /dev/null 2019-12-03 13:57:16.000000000 -0500 +++ new/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/AppImageFileTest.java 2019-12-03 13:57:14.458839600 -0500 @@ -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; + } + +} --- /dev/null 2019-12-03 13:57:24.000000000 -0500 +++ new/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/ApplicationLayoutTest.java 2019-12-03 13:57:22.574974100 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:57:32.000000000 -0500 +++ new/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/CompareDottedVersionTest.java 2019-12-03 13:57:30.427250200 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:57:40.000000000 -0500 +++ new/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/DeployParamsTest.java 2019-12-03 13:57:38.171192400 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:57:48.000000000 -0500 +++ new/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/DottedVersionTest.java 2019-12-03 13:57:46.162208800 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:57:56.000000000 -0500 +++ new/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/InvalidDottedVersionTest.java 2019-12-03 13:57:53.973394600 -0500 @@ -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; +} --- /dev/null 2019-12-03 13:58:04.000000000 -0500 +++ new/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/OverridableResourceTest.java 2019-12-03 13:58:02.161146900 -0500 @@ -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")); + } +} --- /dev/null 2019-12-03 13:58:12.000000000 -0500 +++ new/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/PathGroupTest.java 2019-12-03 13:58:10.161523800 -0500 @@ -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(""); +} --- /dev/null 2019-12-03 13:58:20.000000000 -0500 +++ new/test/jdk/tools/jpackage/junit/jdk/incubator/jpackage/internal/ToolValidatorTest.java 2019-12-03 13:58:18.081322200 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 13:58:28.000000000 -0500 +++ new/test/jdk/tools/jpackage/junit/junit.java 2019-12-03 13:58:26.056628900 -0500 @@ -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 + */ --- /dev/null 2019-12-03 13:58:36.000000000 -0500 +++ new/test/jdk/tools/jpackage/junit/run_junit.sh 2019-12-03 13:58:34.029277300 -0500 @@ -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[@]}" --- /dev/null 2019-12-03 13:58:44.000000000 -0500 +++ new/test/jdk/tools/jpackage/linux/AppCategoryTest.java 2019-12-03 13:58:42.035474000 -0500 @@ -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(); + }); + } +} --- /dev/null 2019-12-03 13:58:52.000000000 -0500 +++ new/test/jdk/tools/jpackage/linux/LicenseTypeTest.java 2019-12-03 13:58:50.067086900 -0500 @@ -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(); + }); + } +} --- /dev/null 2019-12-03 13:59:00.000000000 -0500 +++ new/test/jdk/tools/jpackage/linux/LinuxBundleNameTest.java 2019-12-03 13:58:58.011830500 -0500 @@ -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(); + }); + } +} --- /dev/null 2019-12-03 13:59:08.000000000 -0500 +++ new/test/jdk/tools/jpackage/linux/LinuxResourceTest.java 2019-12-03 13:59:06.091382500 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 13:59:16.000000000 -0500 +++ new/test/jdk/tools/jpackage/linux/MaintainerTest.java 2019-12-03 13:59:14.049343000 -0500 @@ -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(); + }); + } +} --- /dev/null 2019-12-03 13:59:24.000000000 -0500 +++ new/test/jdk/tools/jpackage/linux/PackageDepsTest.java 2019-12-03 13:59:21.964727000 -0500 @@ -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(); + }); + } +} --- /dev/null 2019-12-03 13:59:32.000000000 -0500 +++ new/test/jdk/tools/jpackage/linux/ReleaseTest.java 2019-12-03 13:59:29.778801700 -0500 @@ -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(); + }); + } +} --- /dev/null 2019-12-03 13:59:39.000000000 -0500 +++ new/test/jdk/tools/jpackage/linux/ShortcutHintTest.java 2019-12-03 13:59:37.585712400 -0500 @@ -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(); + }); + } +} --- /dev/null 2019-12-03 13:59:47.000000000 -0500 +++ new/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java 2019-12-03 13:59:45.489438100 -0500 @@ -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)); + } +} --- /dev/null 2019-12-03 13:59:55.000000000 -0500 +++ new/test/jdk/tools/jpackage/macosx/NameWithSpaceTest.java 2019-12-03 13:59:53.419060900 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 14:00:03.000000000 -0500 +++ new/test/jdk/tools/jpackage/macosx/SigningAppImageTest.java 2019-12-03 14:00:01.457769100 -0500 @@ -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"); + }); + } +} --- /dev/null 2019-12-03 14:00:11.000000000 -0500 +++ new/test/jdk/tools/jpackage/macosx/SigningPackageTest.java 2019-12-03 14:00:09.191272400 -0500 @@ -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(); + }); + } +} --- /dev/null 2019-12-03 14:00:19.000000000 -0500 +++ new/test/jdk/tools/jpackage/macosx/base/SigningBase.java 2019-12-03 14:00:16.952526600 -0500 @@ -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); + } + +} --- /dev/null 2019-12-03 14:00:27.000000000 -0500 +++ new/test/jdk/tools/jpackage/macosx/base/SigningCheck.java 2019-12-03 14:00:24.706835400 -0500 @@ -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()); + } + +} --- /dev/null 2019-12-03 14:00:35.000000000 -0500 +++ new/test/jdk/tools/jpackage/manage_packages.sh 2019-12-03 14:00:32.787259700 -0500 @@ -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 Binary files /dev/null and new/test/jdk/tools/jpackage/resources/icon.icns differ Binary files /dev/null and new/test/jdk/tools/jpackage/resources/icon.ico differ Binary files /dev/null and new/test/jdk/tools/jpackage/resources/icon.png differ --- /dev/null 2019-12-03 14:01:06.000000000 -0500 +++ new/test/jdk/tools/jpackage/resources/license.txt 2019-12-03 14:01:04.148589800 -0500 @@ -0,0 +1 @@ +jpackage test license file (just some sample text). --- /dev/null 2019-12-03 14:01:14.000000000 -0500 +++ new/test/jdk/tools/jpackage/run_tests.sh 2019-12-03 14:01:12.061961500 -0500 @@ -0,0 +1,275 @@ +#!/bin/bash + +# +# Script to run jpackage tests. +# + + +# Fail fast +set -e; set -o pipefail; + + +workdir=/tmp/jpackage_jtreg_testing +jtreg_jar=$workdir/jtreg/lib/jtreg.jar +jpackage_test_selector=test/jdk/tools/jpackage + + +find_packaging_tests () +{ + (cd "$open_jdk_with_jpackage_jtreg_tests" && \ + find "$jpackage_test_selector/$1" -type f -name '*.java' \ + | xargs grep -E -l '@key[[:space:]]+jpackagePlatformPackage') +} + + +find_all_packaging_tests () +{ + find_packaging_tests share + case "$(uname -s)" in + Darwin) + find_packaging_tests macosx;; + Linux) + find_packaging_tests linux;; + CYGWIN*|MINGW32*|MSYS*) + find_packaging_tests windows;; + *) + fatal Failed to detect OS type;; + esac +} + + +help_usage () +{ + echo "Usage: `basename $0` [options] [test_names]" + echo "Options:" + echo " -h - print this message" + echo " -v - verbose output" + echo " -c - keep jtreg cache" + echo " -a - run all, not only SQE tests" + echo " -d - dry run. Print jtreg command line, but don't execute it" + echo " -t - 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" + # TODO - restore code to download or copy jtreg.jar + # to $workdir/jtreg/lib/jtreg.jar + fatal "ERROR: All Tests Disabled until locating jtreg.jar implemented." + 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 --- /dev/null 2019-12-03 14:01:22.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/AddLauncherBase.java 2019-12-03 14:01:19.978076400 -0500 @@ -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"); + } + + + } + +} --- /dev/null 2019-12-03 14:01:30.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/AddLauncherModuleTest.java 2019-12-03 14:01:27.762444300 -0500 @@ -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); + } + +} --- /dev/null 2019-12-03 14:01:38.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/AddLauncherTest.java 2019-12-03 14:01:35.978591800 -0500 @@ -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); + } + +} --- /dev/null 2019-12-03 14:01:46.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/AddLaunchersTest.java 2019-12-03 14:01:43.958925700 -0500 @@ -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"); + + } + +} --- /dev/null 2019-12-03 14:01:54.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/AdditionalLaunchersTest.java 2019-12-03 14:01:51.839335100 -0500 @@ -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; + } +} --- /dev/null 2019-12-03 14:02:02.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/AppImagePackageTest.java 2019-12-03 14:01:59.731115800 -0500 @@ -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(); + }); + } +} --- /dev/null 2019-12-03 14:02:10.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/ArgumentsTest.java 2019-12-03 14:02:07.843680900 -0500 @@ -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" + ); +} --- /dev/null 2019-12-03 14:02:17.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/Base.java 2019-12-03 14:02:15.548235800 -0500 @@ -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()); + } +} --- /dev/null 2019-12-03 14:02:25.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/ErrorTest.java 2019-12-03 14:02:23.481348000 -0500 @@ -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); + + } + +} --- /dev/null 2019-12-03 14:02:35.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/FileAssociationsTest.java 2019-12-03 14:02:31.791494100 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 14:02:49.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/IconTest.java 2019-12-03 14:02:46.126737600 -0500 @@ -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)); +} --- /dev/null 2019-12-03 14:03:00.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/InstallDirTest.java 2019-12-03 14:02:57.564167000 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 14:03:11.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/InvalidArgTest.java 2019-12-03 14:03:08.473613200 -0500 @@ -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(); + } + +} --- /dev/null 2019-12-03 14:03:22.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/JavaOptionsBase.java 2019-12-03 14:03:19.436577800 -0500 @@ -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); + } +} --- /dev/null 2019-12-03 14:03:33.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/JavaOptionsEqualsTest.java 2019-12-03 14:03:30.324060300 -0500 @@ -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(); + } + +} --- /dev/null 2019-12-03 14:03:44.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/JavaOptionsModuleTest.java 2019-12-03 14:03:41.341114000 -0500 @@ -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); + } + +} --- /dev/null 2019-12-03 14:03:55.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/JavaOptionsTest.java 2019-12-03 14:03:52.211047200 -0500 @@ -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); + } + +} --- /dev/null 2019-12-03 14:04:06.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/LicenseTest.java 2019-12-03 14:04:02.986163000 -0500 @@ -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")); +} --- /dev/null 2019-12-03 14:04:16.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/MissingArgumentsTest.java 2019-12-03 14:04:13.650609700 -0500 @@ -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(); + } + +} --- /dev/null 2019-12-03 14:04:27.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/RuntimePackageTest.java 2019-12-03 14:04:24.380645500 -0500 @@ -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"); + }); + } +} --- /dev/null 2019-12-03 14:04:38.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/SimplePackageTest.java 2019-12-03 14:04:35.526355700 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 14:04:46.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/jdk/jpackage/tests/AppVersionTest.java 2019-12-03 14:04:44.032583600 -0500 @@ -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; +} --- /dev/null 2019-12-03 14:04:54.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java 2019-12-03 14:04:52.074768800 -0500 @@ -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); + } +} --- /dev/null 2019-12-03 14:05:02.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java 2019-12-03 14:05:00.000347900 -0500 @@ -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; +} --- /dev/null 2019-12-03 14:05:10.000000000 -0500 +++ new/test/jdk/tools/jpackage/share/jdk/jpackage/tests/ModulePathTest.java 2019-12-03 14:05:07.775429100 -0500 @@ -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@"; +} --- /dev/null 2019-12-03 14:05:18.000000000 -0500 +++ new/test/jdk/tools/jpackage/test_jpackage.sh 2019-12-03 14:05:15.624641700 -0500 @@ -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[@]}" --- /dev/null 2019-12-03 14:05:25.000000000 -0500 +++ new/test/jdk/tools/jpackage/windows/WinConsoleTest.java 2019-12-03 14:05:23.501783000 -0500 @@ -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)); + } +} --- /dev/null 2019-12-03 14:05:33.000000000 -0500 +++ new/test/jdk/tools/jpackage/windows/WinDirChooserTest.java 2019-12-03 14:05:31.380920000 -0500 @@ -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(); + }); + } +} --- /dev/null 2019-12-03 14:05:41.000000000 -0500 +++ new/test/jdk/tools/jpackage/windows/WinMenuGroupTest.java 2019-12-03 14:05:39.123746100 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 14:05:49.000000000 -0500 +++ new/test/jdk/tools/jpackage/windows/WinMenuTest.java 2019-12-03 14:05:47.063502900 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 14:05:57.000000000 -0500 +++ new/test/jdk/tools/jpackage/windows/WinPerUserInstallTest.java 2019-12-03 14:05:54.990290300 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 14:06:05.000000000 -0500 +++ new/test/jdk/tools/jpackage/windows/WinResourceTest.java 2019-12-03 14:06:02.860634400 -0500 @@ -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; +} --- /dev/null 2019-12-03 14:06:13.000000000 -0500 +++ new/test/jdk/tools/jpackage/windows/WinScriptTest.java 2019-12-03 14:06:10.792427900 -0500 @@ -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; +} --- /dev/null 2019-12-03 14:06:21.000000000 -0500 +++ new/test/jdk/tools/jpackage/windows/WinShortcutTest.java 2019-12-03 14:06:18.776275900 -0500 @@ -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(); + } +} --- /dev/null 2019-12-03 14:06:29.000000000 -0500 +++ new/test/jdk/tools/jpackage/windows/WinUpgradeUUIDTest.java 2019-12-03 14:06:26.732882300 -0500 @@ -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")); + } +} --- old/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/packager/AppRuntimeImageBuilder.java 2019-12-03 14:06:40.994175100 -0500 +++ /dev/null 2019-12-03 14:06:42.000000000 -0500 @@ -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); - } -}