OpenJDK / jdk / jdk
changeset 57245:9f9e7c969f78
8212780: Packaging Tool Implementation
Reviewed-by: asemenyuk, almatvee, herrick, kcr, prr, erikj, ihse, rriggs, mchung, alanb
Contributed-by: alexey.semenyuk@oracle.com, alexander.matveev@oracle.com, andy.herrick@oracle.com, kevin.rushforth@oracle.com, philip.race@oracle.com
line wrap: on
line diff
--- a/make/CompileJavaModules.gmk Thu Dec 05 15:45:58 2019 +0000 +++ b/make/CompileJavaModules.gmk Thu Dec 05 11:25:33 2019 -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 \
--- a/make/common/Modules.gmk Thu Dec 05 15:45:58 2019 +0000 +++ b/make/common/Modules.gmk Thu Dec 05 11:25:33 2019 -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
--- a/make/common/NativeCompilation.gmk Thu Dec 05 15:45:58 2019 +0000 +++ b/make/common/NativeCompilation.gmk Thu Dec 05 11:25:33 2019 -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) \
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/make/launcher/Launcher-jdk.incubator.jpackage.gmk Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/make/lib/Lib-jdk.incubator.jpackage.gmk Thu Dec 05 11:25:33 2019 -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
--- a/src/java.base/share/classes/module-info.java Thu Dec 05 15:45:58 2019 +0000 +++ b/src/java.base/share/classes/module-info.java Thu Dec 05 11:25:33 2019 -0500 @@ -207,7 +207,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,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/DesktopIntegration.java Thu Dec 05 11:25:33 2019 -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<String, ? super Object> 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<String> requiredPackages() { + return Stream.of(List.of(this), nestedIntegrations).flatMap( + List::stream).map(DesktopIntegration::requiredPackagesSelf).flatMap( + List::stream).distinct().collect(Collectors.toList()); + } + + Map<String, String> create() throws IOException { + associations.forEach(assoc -> assoc.data.verify()); + + if (iconFile != null) { + // Create application icon file. + iconResource.saveToFile(iconFile.srcPath()); + } + + Map<String, String> 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<String> installShellCmds = new ArrayList<>(Arrays.asList( + data.get(DESKTOP_COMMANDS_INSTALL))); + List<String> uninstallShellCmds = new ArrayList<>(Arrays.asList( + data.get(DESKTOP_COMMANDS_UNINSTALL))); + for (var integration: nestedIntegrations) { + if (!integration.associations.isEmpty()) { + needCleanupScripts = true; + } + + Map<String, String> 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<String> requiredPackagesSelf() { + if (desktopFile != null) { + return List.of("xdg-utils"); + } + return Collections.emptyList(); + } + + private Map<String, String> createDataForDesktopFile( + Map<String, ? super Object> params) { + Map<String, String> 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<String, String> data) { + List<String> 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<String> registerIconCmds; + private List<String> 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<String> 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<String, String> data) throws IOException { + List<String> mimeTypes = getMimeTypeNamesFromFileAssociations(); + data.put("DESKTOP_MIMES", "MimeType=" + String.join(";", mimeTypes)); + + // prepare desktop shortcut + desktopFileResource + .setSubstitutionData(data) + .saveToFile(desktopFile.srcPath()); + } + + private List<String> 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<Integer> 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<String> 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<LinuxFileAssociation> associations; + + private final List<Map<String, ? super Object>> launchers; + + private final OverridableResource iconResource; + private final OverridableResource desktopFileResource; + + private final DesktopFile mimeInfoFile; + private final DesktopFile desktopFile; + private final DesktopFile iconFile; + + private final List<DesktopIntegration> nestedIntegrations; + + private final Map<String, String> desktopFileData; + + private static final BundlerParamInfo<String> 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<Boolean> 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LibProvidersLookup.java Thu Dec 05 11:25:33 2019 -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<String> execute(Path root) throws IOException { + // Get the list of files in the root for which to look up for needed shared libraries + List<Path> allPackageFiles; + try (Stream<Path> stream = Files.walk(root)) { + allPackageFiles = stream.filter(Files::isRegularFile).filter( + LibProvidersLookup::canDependOnLibs).collect( + Collectors.toList()); + } + + Collection<Path> neededLibs = getNeededLibsForFiles(allPackageFiles); + + // Get the list of unique package names. + List<String> neededPackages = neededLibs.stream().map(libPath -> { + try { + List<String> 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<String> packageNames = Collections.emptyList(); + return packageNames; + } + }).flatMap(List::stream).sorted().distinct().collect(Collectors.toList()); + + return neededPackages; + } + + private static List<Path> getNeededLibsForFile(Path path) throws IOException { + List<Path> 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<Path> getNeededLibsForFiles(List<Path> paths) { + // Depending on tool used, the set can contain full paths (ldd) or + // only file names (objdump). + Set<Path> allLibs = paths.stream().map(path -> { + List<Path> 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<Path> excludedNames = paths.stream().map(Path::getFileName).collect( + Collectors.toSet()); + Iterator<Path> 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<String> 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppBundler.java Thu Dec 05 11:25:33 2019 -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<File> 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<String> 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<String> LINUX_PACKAGE_DEPENDENCIES = + new StandardBundlerParam<>( + Arguments.CLIOptions.LINUX_PACKAGE_DEPENDENCIES.getId(), + String.class, + params -> { + return ""; + }, + (s, p) -> s + ); + + @Override + public boolean validate(Map<String, ? super Object> 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<String, ? super Object> params) + throws ConfigException { + + imageBundleValidation(params); + + return true; + } + + File doBundle(Map<String, ? super Object> 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<String, ? super Object> 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<String, ? super Object> 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxAppImageBuilder.java Thu Dec 05 11:25:33 2019 -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<File> 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<String, Object> params, + Path imageOutDir) { + return ApplicationLayout.linuxAppImage().resolveAt( + imageOutDir.resolve(APP_NAME.fetchFrom(params))); + } + + public LinuxAppImageBuilder(Map<String, Object> 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<String, ? super Object> params) { + return APP_NAME.fetchFrom(params); + } + + private Path getLauncherCfgPath(Map<String, ? super Object> 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<String, ? super Object> params) + throws IOException { + Map<String, ? super Object> 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<Map<String, ? super Object>> entryPoints + = StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params); + for (Map<String, ? super Object> 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<String, ? super Object> params) + throws IOException {} + + private void createLauncherForEntryPoint( + Map<String, ? super Object> 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<String, ? super Object> 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<String, ? super Object> 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxDebBundler.java Thu Dec 05 11:25:33 2019 -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<String> 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<String> 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<String> EMAIL = + new StandardBundlerParam<> ( + Arguments.CLIOptions.LINUX_DEB_MAINTAINER.getId(), + String.class, + params -> "Unknown", + (s, p) -> s); + + private static final BundlerParamInfo<String> MAINTAINER = + new StandardBundlerParam<> ( + BundleParams.PARAM_MAINTAINER, + String.class, + params -> VENDOR.fetchFrom(params) + " <" + + EMAIL.fetchFrom(params) + ">", + (s, p) -> s); + + private static final BundlerParamInfo<String> SECTION = + new StandardBundlerParam<>( + Arguments.CLIOptions.LINUX_CATEGORY.getId(), + String.class, + params -> "misc", + (s, p) -> s); + + private static final BundlerParamInfo<String> 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<String, ? super Object> 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<ToolValidator> getToolValidators( + Map<String, ? super Object> params) { + return Stream.of(TOOL_DPKG_DEB, TOOL_DPKG, TOOL_FAKEROOT).map( + ToolValidator::new).collect(Collectors.toList()); + } + + @Override + protected File buildPackageBundle( + Map<String, String> replacementData, + Map<String, ? super Object> 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<String, ? super Object> 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 <ubuntu-devel-discuss@lists.ubuntu.com> + // 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<String> archPackages = new HashSet<>(); + Set<String> 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<ConfigException> verifyOutputBundle( + Map<String, ? super Object> params, Path packageBundle) { + List<ConfigException> errors = new ArrayList<>(); + + String controlFileName = "control"; + + List<PackageProperty> 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<String> cmdline = new ArrayList<>(List.of(TOOL_DPKG_DEB, "-f", + packageBundle.toString())); + properties.forEach(property -> cmdline.add(property.name)); + try { + Map<String, String> 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<PosixFilePermission> 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<Path>() { + @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<String, String> data, Map<String, ? super Object> 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<String, String> data, + Map<String, ? super Object> params) throws IOException { + + Path configDir = createMetaPackage(params).sourceRoot().resolve("DEBIAN"); + List<DebianFile> 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<String, String> createReplacementData( + Map<String, ? super Object> params) throws IOException { + Map<String, String> 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<String, ? super Object> 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<String, ? super Object> 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<String> 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxPackageBundler.java Thu Dec 05 11:25:33 2019 -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<String> packageName) { + this.packageName = packageName; + } + + @Override + final public boolean validate(Map<String, ? super Object> 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<String, ? super Object> params, + File outputParentDir) throws PackagerException { + IOUtils.writableOutputDir(outputParentDir.toPath()); + + PlatformPackage thePackage = createMetaPackage(params); + + Function<File, ApplicationLayout> 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<String, String> 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<String> getListOfNeededPackages( + Map<String, ? super Object> params) throws IOException { + + PlatformPackage thePackage = createMetaPackage(params); + + final List<String> xdgUtilsPackage; + if (desktopIntegration != null) { + xdgUtilsPackage = desktopIntegration.requiredPackages(); + } else { + xdgUtilsPackage = Collections.emptyList(); + } + + final List<String> 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<String> 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<String, String> createDefaultReplacementData( + Map<String, ? super Object> params) throws IOException { + Map<String, String> 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<ConfigException> verifyOutputBundle( + Map<String, ? super Object> params, Path packageBundle); + + abstract protected void initLibProvidersLookup( + Map<String, ? super Object> params, + LibProvidersLookup libProvidersLookup); + + abstract protected List<ToolValidator> getToolValidators( + Map<String, ? super Object> params); + + abstract protected void doValidate(Map<String, ? super Object> params) + throws ConfigException; + + abstract protected Map<String, String> createReplacementData( + Map<String, ? super Object> params) throws IOException; + + abstract protected File buildPackageBundle( + Map<String, String> replacementData, + Map<String, ? super Object> params, File outputParentDir) throws + PackagerException, IOException; + + final protected PlatformPackage createMetaPackage( + Map<String, ? super Object> 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<String, ? super Object> 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<Map<String, ? super Object>> associations) throws + ConfigException { + // only one mime type per association, at least one file extention + int assocIdx = 0; + for (var assoc : associations) { + ++assocIdx; + List<String> 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<String> packageName; + private boolean withFindNeededPackages; + private DesktopIntegration desktopIntegration; + + private static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER = + new StandardBundlerParam<>( + "linux.app.bundler", + LinuxAppBundler.class, + (params) -> new LinuxAppBundler(), + null + ); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxRpmBundler.java Thu Dec 05 11:25:33 2019 -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<String> 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<String> 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<String> 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<String, ? super Object> 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<ToolValidator> getToolValidators( + Map<String, ? super Object> params) { + return List.of(createRpmbuildToolValidator()); + } + + @Override + protected File buildPackageBundle( + Map<String, String> replacementData, + Map<String, ? super Object> 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<String, String> createReplacementData( + Map<String, ? super Object> params) throws IOException { + Map<String, String> 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<String, ? super Object> 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<ConfigException> verifyOutputBundle( + Map<String, ? super Object> params, Path packageBundle) { + List<ConfigException> errors = new ArrayList<>(); + + String specFileName = specFile(params).getFileName().toString(); + + try { + List<PackageProperty> 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<String> 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<String> 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<String, ? super Object> params) { + return TEMP_ROOT.fetchFrom(params).toPath().resolve(Path.of("SPECS", + PACKAGE_NAME.fetchFrom(params) + ".spec")); + } + + private Path buildRPM(Map<String, ? super Object> 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/PackageProperty.java Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources.properties Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources_ja.properties Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/LinuxResources_zh_CN.properties Thu Dec 05 11:25:33 2019 -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 file src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/java32.png has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.control Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.copyright Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.desktop Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.postinst Thu Dec 05 11:25:33 2019 -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: +# * <postinst> `configure' <most-recently-configured-version> +# * <old-postinst> `abort-upgrade' <new version> +# * <conflictor's-postinst> `abort-remove' `in-favour' <package> +# <new-version> +# * <postinst> `abort-remove' +# * <deconfigured's-postinst> `abort-deconfigure' `in-favour' +# <failed-install-package> <version> `removing' +# <conflicting-package> <version> +# 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.postrm Thu Dec 05 11:25:33 2019 -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: +# * <postrm> `remove' +# * <postrm> `purge' +# * <old-postrm> `upgrade' <new-version> +# * <new-postrm> `failed-upgrade' <old-version> +# * <new-postrm> `abort-install' +# * <new-postrm> `abort-install' <old-version> +# * <new-postrm> `abort-upgrade' <old-version> +# * <disappearer's-postrm> `disappear' <overwriter> +# <overwriter-version> +# 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.preinst Thu Dec 05 11:25:33 2019 -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: +# * <new-preinst> `install' +# * <new-preinst> `install' <old-version> +# * <new-preinst> `upgrade' <old-version> +# * <old-preinst> `abort-upgrade' <new-version> +# 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.prerm Thu Dec 05 11:25:33 2019 -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: +# * <prerm> `remove' +# * <old-prerm> `upgrade' <new-version> +# * <new-prerm> `failed-upgrade' <old-version> +# * <conflictor's-prerm> `remove' `in-favour' <package> <new-version> +# * <deconfigured's-prerm> `deconfigure' `in-favour' +# <package-being-installed> <version> `removing' +# <conflicting-package> <version> +# 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/template.spec Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/resources/utils.sh Thu Dec 05 11:25:33 2019 -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" <<EOF + BEGIN { + mime_type="$mime_type" + mime_type_regexp="~" mime_type "=" + desktop_file="$desktop_file" + } + \$0 ~ mime_type { + \$0 = substr(\$0, length(mime_type) + 2); + split(\$0, desktop_files, ";") + remaining_desktop_files + counter=0 + for (idx in desktop_files) { + if (desktop_files[idx] != desktop_file) { + ++counter; + } + } + if (counter) { + printf mime_type "=" + for (idx in desktop_files) { + if (desktop_files[idx] != desktop_file) { + printf desktop_files[idx] + if (--counter) { + printf ";" + } + } + } + printf "\n" + } + next + } + + { print } +EOF +} + + +# +# Remove $2 desktop file from the list of default handlers for $@ mime types +# in $1 file. +# Result is saved in $1 file. +# +_uninstall_default_mime_handler () +{ + local defaults_list=$1 + shift + [ -f "$defaults_list" ] || return 0 + + local desktop_file="$1" + shift + + tmpfile1=$(mktemp) + tmpfile2=$(mktemp) + cat "$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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/classes/module-info.java.extra Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/native/jpackageapplauncher/launcher.cpp Thu Dec 05 11:25:33 2019 -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 <dlfcn.h> +#include <locale.h> +#include <string> +#include <libgen.h> +#include <stdio.h> +#include <unistd.h> + + +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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/native/libapplauncher/LinuxPlatform.cpp Thu Dec 05 11:25:33 2019 -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 <stdlib.h> +#include <pwd.h> +#include <sys/file.h> +#include <sys/stat.h> +#include <errno.h> +#include <unistd.h> +#include <sys/types.h> +#include <limits.h> +#include <signal.h> + +#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<TCHAR> 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<TString> 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<TString>::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 <stdio.h> +#include <string.h> +#include <strings.h> +#include <sys/stat.h> + +#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 <sys/types.h> +#include <sys/stat.h> +#include <setjmp.h> +#include <stdlib.h> +#include <wctype.h> + +#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("<!--"), 4) == 0) { + SKIP_CHARS(p, 4); + do { + 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("<!"), 2) == 0) { + SKIP_CHARS(p, 2); + while (*p != '\0') { + if (*p == '>') { + 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); + do { + 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 /* <tag */ +#define TOKEN_END_TAG 2 /* </tag */ +#define TOKEN_CLOSE_BRACKET 3 /* > */ +#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 (<!-- comment -->) 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("</%s>"), 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("<![CDATA["); +const TCHAR *CDEnd = _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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/native/libapplauncher/LinuxPlatform.h Thu Dec 05 11:25:33 2019 -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 <X11/Xlib.h> +#include <X11/Xatom.h> +#include <pthread.h> +#include <list> + +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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/linux/native/libapplauncher/PlatformDefs.h Thu Dec 05 11:25:33 2019 -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 <errno.h> +#include <unistd.h> +#include <sys/stat.h> +#include <dlfcn.h> +#include <libgen.h> +#include <string> + +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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/EnumeratedBundlerParam.java Thu Dec 05 11:25:33 2019 -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<T> + * + * 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: + * + * <pre>{@code + * Set<String> 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); + * }</pre> + * + */ +class EnumeratedBundlerParam<T> extends BundlerParamInfo<T> { + // 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<String, T> elements; + private final boolean strict; + + EnumeratedBundlerParam(String id, Class<T> valueType, + Function<Map<String, ? super Object>, T> defaultValueFunction, + BiFunction<String, Map<String, ? super Object>, T> stringConverter, + Map<String, T> 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<String> 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppBundler.java Thu Dec 05 11:25:33 2019 -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<String> MAC_CF_BUNDLE_NAME = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(), + String.class, + params -> null, + (s, p) -> s); + + public static final BundlerParamInfo<String> 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<String> DEFAULT_ICNS_ICON = + new StandardBundlerParam<>( + ".mac.default.icns", + String.class, + params -> TEMPLATE_BUNDLE_ICON, + (s, p) -> s); + + public static final BundlerParamInfo<String> 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<String> 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<File> 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<String, ? super Object> 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<String, ? super Object> 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<String, ? super Object> 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<String, ? super Object> 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<String, ? super Object> 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppImageBuilder.java Thu Dec 05 11:25:33 2019 -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<String> keyChains; + + public static final BundlerParamInfo<Boolean> + 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<String> MAC_CF_BUNDLE_NAME = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(), + String.class, + params -> null, + (s, p) -> s); + + public static final BundlerParamInfo<String> 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<String> 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<File> 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<Boolean> 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<String, Object> 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<String, ? super Object> params) + throws IOException { + Map<String, ? super Object> 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<Map<String, ? super Object>> entryPoints = + StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params); + for (Map<String, ? super Object> entryPoint : entryPoints) { + Map<String, ? super Object> 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<String, ? + super Object> 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<String, ? super Object> 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<String, ? super Object> 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<String, ? super Object> 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<String, ? super Object> params) { + if (APP_NAME.fetchFrom(params) != null) { + return APP_NAME.fetchFrom(params); + } else { + return MAIN_CLASS.fetchFrom(params); + } + } + + public static String getLauncherCfgName( + Map<String, ? super Object> params) { + return "Contents/app/" + APP_NAME.fetchFrom(params) + ".cfg"; + } + + private void copyClassPathEntries(Path javaDirectory, + Map<String, ? super Object> params) throws IOException { + List<RelativeFileSet> 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<String, ? super Object> 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<String, ? super Object> params) throws IOException { + Map<String, String> 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<String, ? super Object> 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<String, String> 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<String> 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( + " <string>").append(o).append("</string>"); + newline = "\n"; + } + + data.put("DEPLOY_JAVA_OPTIONS", sb.toString()); + + sb = new StringBuilder(); + List<String> args = ARGUMENTS.fetchFrom(params); + newline = ""; + // So we don't add unneccessary extra line after last append + + for (String o : args) { + sb.append(newline).append(" <string>").append(o).append( + "</string>"); + 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<String, ? super Object> + fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) { + + List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); + + if (extensions == null) { + Log.verbose(I18N.getString( + "message.creating-association-with-null-extension")); + } + + List<String> 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(" <dict>\n") + .append(" <key>LSItemContentTypes</key>\n") + .append(" <array>\n") + .append(" <string>") + .append(itemContentType) + .append("</string>\n") + .append(" </array>\n") + .append("\n") + .append(" <key>CFBundleTypeName</key>\n") + .append(" <string>") + .append(description) + .append("</string>\n") + .append("\n") + .append(" <key>LSHandlerRank</key>\n") + .append(" <string>Owner</string>\n") + // TODO make a bundler arg + .append("\n") + .append(" <key>CFBundleTypeRole</key>\n") + .append(" <string>Editor</string>\n") + // TODO make a bundler arg + .append("\n") + .append(" <key>LSIsAppleDefaultForType</key>\n") + .append(" <true/>\n") + // TODO make a bundler arg + .append("\n"); + + if (icon != null && icon.exists()) { + bundleDocumentTypes + .append(" <key>CFBundleTypeIconFile</key>\n") + .append(" <string>") + .append(icon.getName()) + .append("</string>\n"); + } + bundleDocumentTypes.append(" </dict>\n"); + + exportedTypes.append(" <dict>\n") + .append(" <key>UTTypeIdentifier</key>\n") + .append(" <string>") + .append(itemContentType) + .append("</string>\n") + .append("\n") + .append(" <key>UTTypeDescription</key>\n") + .append(" <string>") + .append(description) + .append("</string>\n") + .append(" <key>UTTypeConformsTo</key>\n") + .append(" <array>\n") + .append(" <string>public.data</string>\n") + //TODO expose this? + .append(" </array>\n") + .append("\n"); + + if (icon != null && icon.exists()) { + exportedTypes.append(" <key>UTTypeIconFile</key>\n") + .append(" <string>") + .append(icon.getName()) + .append("</string>\n") + .append("\n"); + } + + exportedTypes.append("\n") + .append(" <key>UTTypeTagSpecification</key>\n") + .append(" <dict>\n") + // TODO expose via param? .append( + // " <key>com.apple.ostype</key>\n"); + // TODO expose via param? .append( + // " <string>ABCD</string>\n") + .append("\n"); + + if (extensions != null && !extensions.isEmpty()) { + exportedTypes.append( + " <key>public.filename-extension</key>\n") + .append(" <array>\n"); + + for (String ext : extensions) { + exportedTypes.append(" <string>") + .append(ext) + .append("</string>\n"); + } + exportedTypes.append(" </array>\n"); + } + if (mimeTypes != null && !mimeTypes.isEmpty()) { + exportedTypes.append(" <key>public.mime-type</key>\n") + .append(" <array>\n"); + + for (String mime : mimeTypes) { + exportedTypes.append(" <string>") + .append(mime) + .append("</string>\n"); + } + exportedTypes.append(" </array>\n"); + } + exportedTypes.append(" </dict>\n") + .append(" </dict>\n"); + } + String associationData; + if (bundleDocumentTypes.length() > 0) { + associationData = + "\n <key>CFBundleDocumentTypes</key>\n <array>\n" + + bundleDocumentTypes.toString() + + " </array>\n\n" + + " <key>UTExportedTypeDeclarations</key>\n <array>\n" + + exportedTypes.toString() + + " </array>\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<String, ? super Object> 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<String> 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<String> 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<String, ? super Object> 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<String> 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<String, ? super Object> params, Path appLocation, + String signingIdentity, String identifierPrefix, + String entitlementsFile, String inheritedEntitlements) + throws IOException { + AtomicReference<IOException> toThrow = new AtomicReference<>(); + String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params); + String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); + + // sign all dylibs and jars + try (Stream<Path> stream = Files.walk(appLocation)) { + stream.peek(path -> { // fix permissions + try { + Set<PosixFilePermission> 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<String> 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<PosixFilePermission> 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<? super Path> signIdentifiedByPList = path -> { + //noinspection ThrowableResultOfMethodCallIgnored + if (toThrow.get() != null) return; + + try { + List<String> 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<String> 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<String, Object> 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 <string> element preceding <key> + // 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacAppStoreBundler.java Thu Dec 05 11:25:33 2019 -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<String> 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<String> 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<File> 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<String> INSTALLER_SUFFIX = + new StandardBundlerParam<> ( + "mac.app-store.installerName.suffix", + String.class, + params -> "-MacAppStore", + (s, p) -> s); + + public File bundle(Map<String, ? super Object> 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<String> 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<String, ? super Object> params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + ".entitlements"); + } + + private File getConfig_Inherit_Entitlements( + Map<String, ? super Object> params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "_Inherit.entitlements"); + } + + private void prepareEntitlements(Map<String, ? super Object> 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<String, ? super Object> 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<String, ? super Object> 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacBaseInstallerBundler.java Thu Dec 05 11:25:33 2019 -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<MacAppBundler> APP_BUNDLER = + new StandardBundlerParam<>( + "mac.app.bundler", + MacAppBundler.class, + params -> new MacAppBundler(), + (s, p) -> null); + + public final BundlerParamInfo<File> 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<String> SIGNING_KEY_USER = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAC_SIGNING_KEY_NAME.getId(), + String.class, + params -> "", + null); + + public static final BundlerParamInfo<String> SIGNING_KEYCHAIN = + new StandardBundlerParam<>( + Arguments.CLIOptions.MAC_SIGNING_KEYCHAIN.getId(), + String.class, + params -> "", + null); + + public static final BundlerParamInfo<String> 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<String, ? super Object> 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<String, ? super Object> 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<String> 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\"<blob>=\"([^\"]+)\""); + 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacCertificate.java Thu Dec 05 11:25:33 2019 -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<String> 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<String> 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacDmgBundler.java Thu Dec 05 11:25:33 2019 -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<String> INSTALLER_SUFFIX = + new StandardBundlerParam<> ( + "mac.dmg.installerName.suffix", + String.class, + params -> "", + (s, p) -> s); + + public File bundle(Map<String, ? super Object> 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<String, ? super Object> 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<String, String> 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<String, ? super Object> params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-dmg-setup.scpt"); + } + + private File getConfig_VolumeBackground( + Map<String, ? super Object> params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-background.png"); + } + + private File getConfig_VolumeIcon(Map<String, ? super Object> params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-volume.icns"); + } + + private File getConfig_LicenseFile(Map<String, ? super Object> params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-license.plist"); + } + + private void prepareLicense(Map<String, ? super Object> 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<String, String> 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<String, ? super Object> 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<String, ? super Object> 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<String, ? super Object> 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<String, ? super Object> 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<String, ? super Object> 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/MacPkgBundler.java Thu Dec 05 11:25:33 2019 -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<File> 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<File> 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<String> 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<String> 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<String> INSTALLER_SUFFIX = + new StandardBundlerParam<> ( + "mac.pkg.installerName.suffix", + String.class, + params -> "", + (s, p) -> s); + + public File bundle(Map<String, ? super Object> 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<String, ? super Object> params) { + return new File(PACKAGES_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-app.pkg"); + } + + private File getConfig_DistributionXMLFile( + Map<String, ? super Object> params) { + return new File(CONFIG_ROOT.fetchFrom(params), "distribution.dist"); + } + + private File getConfig_BackgroundImage(Map<String, ? super Object> params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-background.png"); + } + + private File getConfig_BackgroundImageDarkAqua(Map<String, ? super Object> params) { + return new File(CONFIG_ROOT.fetchFrom(params), + APP_NAME.fetchFrom(params) + "-background-darkAqua.png"); + } + + private File getScripts_PreinstallFile(Map<String, ? super Object> params) { + return new File(SCRIPTS_DIR.fetchFrom(params), "preinstall"); + } + + private File getScripts_PostinstallFile( + Map<String, ? super Object> params) { + return new File(SCRIPTS_DIR.fetchFrom(params), "postinstall"); + } + + private String getAppIdentifier(Map<String, ? super Object> params) { + return MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params); + } + + private void preparePackageScripts(Map<String, ? super Object> params) + throws IOException { + Log.verbose(I18N.getString("message.preparing-scripts")); + + Map<String, String> 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<String, ? super Object> 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(); // </pkg-ref> + xml.writeStartElement("options"); + xml.writeAttribute("customize", "never"); + xml.writeAttribute("require-scripts", "false"); + xml.writeEndElement(); // </options> + xml.writeStartElement("choices-outline"); + xml.writeStartElement("line"); + xml.writeAttribute("choice", "default"); + xml.writeStartElement("line"); + xml.writeAttribute("choice", appId); + xml.writeEndElement(); // </line> + xml.writeEndElement(); // </line> + xml.writeEndElement(); // </choices-outline> + xml.writeStartElement("choice"); + xml.writeAttribute("id", "default"); + xml.writeEndElement(); // </choice> + xml.writeStartElement("choice"); + xml.writeAttribute("id", appId); + xml.writeAttribute("visible", "false"); + xml.writeStartElement("pkg-ref"); + xml.writeAttribute("id", appId); + xml.writeEndElement(); // </pkg-ref> + xml.writeEndElement(); // </choice> + 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(); // </pkg-ref> + + xml.writeEndElement(); // </installer-gui-script> + }); + } + + private boolean prepareConfigFiles(Map<String, ? super Object> 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<String, ? super Object> 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("<key>BundleIsRelocatable</key>")) { + out.println(lines[i]); + out.println("<false/>"); + i++; + } else if (lines[i].trim().equals("<key>ChildBundles</key>")) { + ++skip; + } else if ((skip > 0) && lines[i].trim().equals("</array>")) { + --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<String, ? super Object> 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<String, ? super Object> 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<String> 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<String, ? super Object> 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<String, ? super Object> 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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/DMGsetup.scpt Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/Info-lite.plist.template Thu Dec 05 11:25:33 2019 -0500 @@ -0,0 +1,37 @@ +<?xml version="1.0" ?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>LSMinimumSystemVersion</key> + <string>10.9</string> + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + <key>CFBundleAllowMixedLocalizations</key> + <true/> + <key>CFBundleExecutable</key> + <string>DEPLOY_LAUNCHER_NAME</string> + <key>CFBundleIconFile</key> + <string>DEPLOY_ICON_FILE</string> + <key>CFBundleIdentifier</key> + <string>DEPLOY_BUNDLE_IDENTIFIER</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>DEPLOY_BUNDLE_NAME</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>DEPLOY_BUNDLE_SHORT_VERSION</string> + <key>CFBundleSignature</key> + <string>????</string> + <!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories --> + <key>LSApplicationCategoryType</key> + <string>Unknown</string> + <key>CFBundleVersion</key> + <string>DEPLOY_BUNDLE_CFBUNDLE_VERSION</string> + <key>NSHumanReadableCopyright</key> + <string>DEPLOY_BUNDLE_COPYRIGHT</string>DEPLOY_FILE_ASSOCIATIONS + <key>NSHighResolutionCapable</key> + <string>true</string> + </dict> +</plist>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacAppStore.entitlements Thu Dec 05 11:25:33 2019 -0500 @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>com.apple.security.app-sandbox</key> + <true/> + </dict> +</plist>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacAppStore_Inherit.entitlements Thu Dec 05 11:25:33 2019 -0500 @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>com.apple.security.app-sandbox</key> + <true/> + <key>com.apple.security.inherit</key> + <true/> + </dict> +</plist>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources.properties Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources_ja.properties Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/MacResources_zh_CN.properties Thu Dec 05 11:25:33 2019 -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 Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/Runtime-Info.plist.template Thu Dec 05 11:25:33 2019 -0500 @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + <key>CFBundleExecutable</key> + <string>libjli.dylib</string> + <key>CFBundleIdentifier</key> + <string>CF_BUNDLE_IDENTIFIER</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>7.0</string> + <key>CFBundleName</key> + <string>CF_BUNDLE_NAME</string> + <key>CFBundlePackageType</key> + <string>BNDL</string> + <key>CFBundleShortVersionString</key> + <string>CF_BUNDLE_SHORT_VERSION_STRING</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>CF_BUNDLE_VERSION</string> +</dict> +</plist>
Binary file src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/background_dmg.png has changed
Binary file src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/background_pkg.png has changed
Binary file src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/java.icns has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.incubator.jpackage/macosx/classes/jdk/incubator/jpackage/internal/resources/lic_template.plist Thu Dec 05 11:25:33 2019 -0500 @@ -0,0 +1,244 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>LPic</key> + <array> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAAAAgAAAAAAAAAAAAQAAA==</data> + <key>ID</key> + <string>5000</string> + <key>Name</key> + <string></string> + </dict> + </array> + <key>STR#</key> + <array> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYPRW5nbGlzaCBkZWZhdWx0BUFncmVlCERpc2FncmVlBVByaW50B1NhdmUuLi56SWYgeW91IGFncmVlIHdpdGggdGhlIHRlcm1zIG9mIHRoaXMgbGljZW5zZSwgY2xpY2sgIkFncmVlIiB0byBhY2Nlc3MgdGhlIHNvZnR3YXJlLiAgSWYgeW91IGRvIG5vdCBhZ3JlZSwgcHJlc3MgIkRpc2FncmVlLiI=</data> + <key>ID</key> + <string>5000</string> + <key>Name</key> + <string>English buttons</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYHRGV1dHNjaAtBa3plcHRpZXJlbghBYmxlaG5lbgdEcnVja2VuClNpY2hlcm4uLi7nS2xpY2tlbiBTaWUgaW4g0kFremVwdGllcmVu0ywgd2VubiBTaWUgbWl0IGRlbiBCZXN0aW1tdW5nZW4gZGVzIFNvZnR3YXJlLUxpemVuenZlcnRyYWdzIGVpbnZlcnN0YW5kZW4gc2luZC4gRmFsbHMgbmljaHQsIGJpdHRlINJBYmxlaG5lbtMgYW5rbGlja2VuLiBTaWUga5pubmVuIGRpZSBTb2Z0d2FyZSBudXIgaW5zdGFsbGllcmVuLCB3ZW5uIFNpZSDSQWt6ZXB0aWVyZW7TIGFuZ2VrbGlja3QgaGFiZW4u</data> + <key>ID</key> + <string>5001</string> + <key>Name</key> + <string>German</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYHRW5nbGlzaAVBZ3JlZQhEaXNhZ3JlZQVQcmludAdTYXZlLi4ue0lmIHlvdSBhZ3JlZSB3aXRoIHRoZSB0ZXJtcyBvZiB0aGlzIGxpY2Vuc2UsIHByZXNzICJBZ3JlZSIgdG8gaW5zdGFsbCB0aGUgc29mdHdhcmUuICBJZiB5b3UgZG8gbm90IGFncmVlLCBwcmVzcyAiRGlzYWdyZWUiLg==</data> + <key>ID</key> + <string>5002</string> + <key>Name</key> + <string>English</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYHRXNwYZZvbAdBY2VwdGFyCk5vIGFjZXB0YXIISW1wcmltaXIKR3VhcmRhci4uLsBTaSBlc3SHIGRlIGFjdWVyZG8gY29uIGxvcyB0jnJtaW5vcyBkZSBlc3RhIGxpY2VuY2lhLCBwdWxzZSAiQWNlcHRhciIgcGFyYSBpbnN0YWxhciBlbCBzb2Z0d2FyZS4gRW4gZWwgc3VwdWVzdG8gZGUgcXVlIG5vIGVzdI4gZGUgYWN1ZXJkbyBjb24gbG9zIHSOcm1pbm9zIGRlIGVzdGEgbGljZW5jaWEsIHB1bHNlICJObyBhY2VwdGFyLiI=</data> + <key>ID</key> + <string>5003</string> + <key>Name</key> + <string>Spanish</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYIRnJhbo1haXMIQWNjZXB0ZXIHUmVmdXNlcghJbXByaW1lcg5FbnJlZ2lzdHJlci4uLrpTaSB2b3VzIGFjY2VwdGV6IGxlcyB0ZXJtZXMgZGUgbGEgcHKOc2VudGUgbGljZW5jZSwgY2xpcXVleiBzdXIgIkFjY2VwdGVyIiBhZmluIGQnaW5zdGFsbGVyIGxlIGxvZ2ljaWVsLiBTaSB2b3VzIG4nkHRlcyBwYXMgZCdhY2NvcmQgYXZlYyBsZXMgdGVybWVzIGRlIGxhIGxpY2VuY2UsIGNsaXF1ZXogc3VyICJSZWZ1c2VyIi4=</data> + <key>ID</key> + <string>5004</string> + <key>Name</key> + <string>French</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYISXRhbGlhbm8HQWNjZXR0bwdSaWZpdXRvBlN0YW1wYQtSZWdpc3RyYS4uLn9TZSBhY2NldHRpIGxlIGNvbmRpemlvbmkgZGkgcXVlc3RhIGxpY2VuemEsIGZhaSBjbGljIHN1ICJBY2NldHRvIiBwZXIgaW5zdGFsbGFyZSBpbCBzb2Z0d2FyZS4gQWx0cmltZW50aSBmYWkgY2xpYyBzdSAiUmlmaXV0byIu</data> + <key>ID</key> + <string>5005</string> + <key>Name</key> + <string>Italian</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYISmFwYW5lc2UKk6+I04K1gtyCtwyTr4jTgrWC3IK5gvEIiPON/IK3gukHlduRti4uLrSWe4Ncg3SDZ4NFg0eDQY5nl3CLlpH4jF+W8YLMj/CMj4LJk6+I04KzguqC6Y/qjYeCyYLNgUGDXIN0g2eDRYNHg0GC8INDg5ODWINngVuDi4K3gumCvYLfgsmBdZOviNOCtYLcgreBdoLwiZ+CtYLEgq2CvoKzgqKBQoFAk6+I04KzguqCyIKij+qNh4LJgs2BQYF1k6+I04K1gtyCuYLxgXaC8ImfgrWCxIKtgr6Cs4KigUI=</data> + <key>ID</key> + <string>5006</string> + <key>Name</key> + <string>Japanese</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYKTmVkZXJsYW5kcwJKYQNOZWUFUHJpbnQJQmV3YWFyLi4upEluZGllbiB1IGFra29vcmQgZ2FhdCBtZXQgZGUgdm9vcndhYXJkZW4gdmFuIGRlemUgbGljZW50aWUsIGt1bnQgdSBvcCAnSmEnIGtsaWtrZW4gb20gZGUgcHJvZ3JhbW1hdHV1ciB0ZSBpbnN0YWxsZXJlbi4gSW5kaWVuIHUgbmlldCBha2tvb3JkIGdhYXQsIGtsaWt0IHUgb3AgJ05lZScu</data> + <key>ID</key> + <string>5007</string> + <key>Name</key> + <string>Dutch</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYGU3ZlbnNrCEdvZGuKbm5zBkF2YppqcwhTa3JpdiB1dAhTcGFyYS4uLpNPbSBEdSBnb2Rrim5uZXIgbGljZW5zdmlsbGtvcmVuIGtsaWNrYSBwjCAiR29ka4pubnMiIGaaciBhdHQgaW5zdGFsbGVyYSBwcm9ncmFtcHJvZHVrdGVuLiBPbSBEdSBpbnRlIGdvZGuKbm5lciBsaWNlbnN2aWxsa29yZW4sIGtsaWNrYSBwjCAiQXZimmpzIi4=</data> + <key>ID</key> + <string>5008</string> + <key>Name</key> + <string>Swedish</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYRUG9ydHVndZBzLCBCcmFzaWwJQ29uY29yZGFyCURpc2NvcmRhcghJbXByaW1pcglTYWx2YXIuLi6MU2UgZXN0hyBkZSBhY29yZG8gY29tIG9zIHRlcm1vcyBkZXN0YSBsaWNlbo1hLCBwcmVzc2lvbmUgIkNvbmNvcmRhciIgcGFyYSBpbnN0YWxhciBvIHNvZnR3YXJlLiBTZSBui28gZXN0hyBkZSBhY29yZG8sIHByZXNzaW9uZSAiRGlzY29yZGFyIi4=</data> + <key>ID</key> + <string>5009</string> + <key>Name</key> + <string>Brazilian Portuguese</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYSU2ltcGxpZmllZCBDaGluZXNlBM2s0uIGsrvNrNLiBLTy06EGtOa0oqGtVMjnufvE+s2s0uKxvtDtv8nQrdLptcTM9b/uo6zH67C0obDNrNLiobHAtLCy17C0y8jtvP6ho8jnufvE+rK7zazS4qOsx+uwtKGwsrvNrNLiobGhow==</data> + <key>ID</key> + <string>5010</string> + <key>Name</key> + <string>Simplified Chinese</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYTVHJhZGl0aW9uYWwgQ2hpbmVzZQSmULdOBqSjplC3TgSmQ6ZMBsB4pnOhS1CmcKpHsXqmULdOpbuzXKVpw9K4zKq6sfi02qFBvdCr9qGnplC3TqGopUimd7jLs27F6aFDpnCqR6SjplC3TqFBvdCr9qGnpKOmULdOoaihQw==</data> + <key>ID</key> + <string>5011</string> + <key>Name</key> + <string>Traditional Chinese</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYFRGFuc2sERW5pZwVVZW5pZwdVZHNrcml2CkFya2l2ZXIuLi6YSHZpcyBkdSBhY2NlcHRlcmVyIGJldGluZ2Vsc2VybmUgaSBsaWNlbnNhZnRhbGVuLCBza2FsIGR1IGtsaWtrZSBwjCDSRW5pZ9MgZm9yIGF0IGluc3RhbGxlcmUgc29mdHdhcmVuLiBLbGlrIHCMINJVZW5pZ9MgZm9yIGF0IGFubnVsbGVyZSBpbnN0YWxsZXJpbmdlbi4=</data> + <key>ID</key> + <string>5012</string> + <key>Name</key> + <string>Danish</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYFU3VvbWkISHl2imtzeW4KRW4gaHl2imtzeQdUdWxvc3RhCVRhbGxlbm5hyW9IeXaKa3N5IGxpc2Vuc3Npc29waW11a3NlbiBlaGRvdCBvc29pdHRhbWFsbGEg1Uh5doprc3nVLiBKb3MgZXQgaHl2imtzeSBzb3BpbXVrc2VuIGVodG9qYSwgb3NvaXRhINVFbiBoeXaKa3N51S4=</data> + <key>ID</key> + <string>5013</string> + <key>Name</key> + <string>Finnish</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYRRnJhbo1haXMgY2FuYWRpZW4IQWNjZXB0ZXIHUmVmdXNlcghJbXByaW1lcg5FbnJlZ2lzdHJlci4uLrpTaSB2b3VzIGFjY2VwdGV6IGxlcyB0ZXJtZXMgZGUgbGEgcHKOc2VudGUgbGljZW5jZSwgY2xpcXVleiBzdXIgIkFjY2VwdGVyIiBhZmluIGQnaW5zdGFsbGVyIGxlIGxvZ2ljaWVsLiBTaSB2b3VzIG4nkHRlcyBwYXMgZCdhY2NvcmQgYXZlYyBsZXMgdGVybWVzIGRlIGxhIGxpY2VuY2UsIGNsaXF1ZXogc3VyICJSZWZ1c2VyIi4=</data> + <key>ID</key> + <string>5014</string> + <key>Name</key> + <string>French Canadian</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYGS29yZWFuBLW/wMcJtb/AxyC+yMfUBsfBuLDGrgfA+sDlLi4ufrvnv+sgsOi+4LytwMcgs7u/67+hILW/wMfHz7jpLCAitb/AxyIgtNzD37imILStt68gvNLHwcauv/6+7rimILyzxKHHz73KvcO/wC4gtb/Ax8fPwfYgvsq0wrTZuOksICK1v8DHIL7Ix9QiILTcw9+4piC0qbijvcq9w7/ALg==</data> + <key>ID</key> + <string>5015</string> + <key>Name</key> + <string>Korean</string> + </dict> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>AAYFTm9yc2sERW5pZwlJa2tlIGVuaWcIU2tyaXYgdXQKQXJraXZlci4uLqNIdmlzIERlIGVyIGVuaWcgaSBiZXN0ZW1tZWxzZW5lIGkgZGVubmUgbGlzZW5zYXZ0YWxlbiwga2xpa2tlciBEZSBwjCAiRW5pZyIta25hcHBlbiBmb3IgjCBpbnN0YWxsZXJlIHByb2dyYW12YXJlbi4gSHZpcyBEZSBpa2tlIGVyIGVuaWcsIGtsaWtrZXIgRGUgcIwgIklra2UgZW5pZyIu</data> + <key>ID</key> + <string>5016</string> + <key>Name</key> + <string>Norwegian</string> + </dict> + </array> + <key>TEXT</key> + <array> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>APPLICATION_LICENSE_TEXT</data> + <key>ID</key> + <string>5000</string> + <key>Name</key> + <string>English SLA</string> + </dict> + </array> + <key>TMPL</key> + <array> + <dict> + <key>Attributes</key> + <string>0x0000</string> + <key>Data</key> + <data>E0RlZmF1bHQgTGFuZ3VhZ2UgSUREV1JEBUNvdW50T0NOVAQqKioqTFNUQwtzeXMgbGFuZyBJRERXUkQebG9jYWwgcmVzIElEIChvZmZzZXQgZnJvbSA1MDAwRFdSRBAyLWJ5dGUgbGFuZ3VhZ2U/RFdSRAQqKioqTFNURQ==</data> + <key>ID</key> + <string>128</string> + <key>Name</key>