Merge pull request #51 from neilcsmith-net/nbpackage-nb14
NBPackage updates - macOS, templates, utilities
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/FileUtils.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/FileUtils.java
index 0d7f27d..bcbfa81 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/FileUtils.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/FileUtils.java
@@ -19,11 +19,13 @@
package org.apache.netbeans.nbpackage;
import java.io.IOException;
+import java.net.URI;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.PathMatcher;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
@@ -31,6 +33,7 @@
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.util.List;
+import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.compress.archivers.ArchiveException;
@@ -141,6 +144,9 @@
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
+ if (dir.equals(src)) {
+ return CONTINUE;
+ }
Path targetDir = dst.resolve(src.relativize(dir));
try {
Files.copy(dir, targetDir);
@@ -165,6 +171,9 @@
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
throws IOException {
+ if (dir.equals(src)) {
+ return CONTINUE;
+ }
Files.delete(dir);
return CONTINUE;
}
@@ -173,6 +182,51 @@
}
/**
+ * Recursively delete a directory and all files inside it.
+ *
+ * @param dir directory to delete
+ * @throws IOException
+ */
+ public static void deleteFiles(Path dir) throws IOException {
+ Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ Files.delete(dir);
+ return FileVisitResult.CONTINUE;
+ }
+
+ });
+ }
+
+ /**
+ * Find all files that match the provided glob pattern within a directory.
+ * Files will be included if their relative path within the directory, or
+ * their filename, match the pattern.
+ *
+ * @param searchDir directory to search within
+ * @param pattern pattern to search for
+ * @return list of files that match pattern
+ * @throws IOException
+ */
+ public static List<Path> find(Path searchDir, String pattern) throws IOException {
+ var matcher = searchDir.getFileSystem().getPathMatcher("glob:" + pattern);
+ try (var stream = Files.find(searchDir, Integer.MAX_VALUE, (file, attr)
+ -> !file.equals(searchDir) &&
+ (matcher.matches(searchDir.relativize(file)) ||
+ matcher.matches(file.getFileName()))
+ )) {
+ return stream.collect(Collectors.toList());
+ }
+ }
+
+ /**
* Find the directories in the given search directory that contains files
* that match the given glob patterns. This might include the search
* directory itself. eg. to find NetBeans or a RCP application in a search
@@ -207,6 +261,77 @@
}
/**
+ * Process files inside a JAR file. This can be used, for example, to sign
+ * native executables inside a JAR. Files whose path inside the JAR matches
+ * the given pattern will be extracted into a temporary directory and passed
+ * to the provided processor. If the processor returns true the file will be
+ * updated inside the JAR.
+ *
+ * @param jarFile JAR file
+ * @param pattern glob pattern to match paths for processing
+ * @param processor process files
+ * @return true if the JAR contents have been updated
+ * @throws IOException
+ */
+ public static boolean processJarContents(Path jarFile, String pattern, JarProcessor processor)
+ throws IOException {
+ URI jarURI = URI.create("jar:" + jarFile.toUri());
+ boolean updated = false;
+ try (var jarFS = FileSystems.newFileSystem(jarURI, Map.of())) {
+ PathMatcher matcher = jarFS.getPathMatcher("glob:" + pattern);
+ List<Path> filesToProcess;
+ try (var jarStream = Files.walk(jarFS.getPath("/"))) {
+ filesToProcess = jarStream
+ .filter(file -> Files.isRegularFile(file))
+ .filter(matcher::matches)
+ .collect(Collectors.toList());
+ }
+ if (filesToProcess.isEmpty()) {
+ return false;
+ }
+ Path tmpDir = Files.createTempDirectory("nbpackage-jar-");
+ try {
+ for (Path file : filesToProcess) {
+ Path tmp = null;
+ try {
+ tmp = Files.copy(file, tmpDir.resolve(file.getFileName().toString()));
+ boolean processed = processor.processFile(tmp, file.toString());
+ if (processed) {
+ Files.copy(tmp, file, StandardCopyOption.REPLACE_EXISTING);
+ updated = true;
+ }
+ } finally {
+ Files.deleteIfExists(tmp);
+ }
+ }
+
+ } finally {
+ Files.delete(tmpDir);
+ }
+ }
+ return updated;
+ }
+
+ /**
+ * Processor for updating files inside a JAR. For use with
+ * {@link FileUtils#processJarContents(java.nio.file.Path, java.lang.String, org.apache.netbeans.nbpackage.FileUtils.JarProcessor)}.
+ */
+ @FunctionalInterface
+ public static interface JarProcessor {
+
+ /**
+ * Process file from JAR.
+ *
+ * @param tmpFile path to temporary extracted file
+ * @param jarPath full path inside JAR
+ * @return true to update the file in the JAR
+ * @throws IOException
+ */
+ public boolean processFile(Path tmpFile, String jarPath) throws IOException;
+
+ }
+
+ /**
* Remove the extension from the given file name, if one exists. Simply
* removes the last dot and remaining characters.
*
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Main.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Main.java
index d7efbbb..7a2c274 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Main.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Main.java
@@ -21,7 +21,6 @@
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Path;
-import java.text.MessageFormat;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.Callable;
@@ -72,6 +71,10 @@
@CommandLine.Option(names = {"--save-config"},
descriptionKey = "option.saveconfig.description")
private Path configOut;
+
+ @CommandLine.Option(names = {"--save-templates"},
+ descriptionKey = "option.savetemplates.description")
+ private Path templatesOut;
@CommandLine.Option(names = {"--image-only"},
descriptionKey = "option.imageonly.description")
@@ -88,7 +91,7 @@
@Override
public Integer call() throws Exception {
try {
- if (input == null && inputImage == null && configOut == null) {
+ if (input == null && inputImage == null && !hasAuxTasks()) {
warning(NBPackage.MESSAGES.getString("message.notasks"));
return 1;
}
@@ -124,6 +127,10 @@
NBPackage.writeFullConfiguration(conf, configOut);
}
+ if (templatesOut != null) {
+ NBPackage.copyTemplates(conf, templatesOut);
+ }
+
Path dest = output == null ? Path.of("") : output;
Path created = null;
@@ -160,6 +167,10 @@
);
System.out.println(ansiMsg);
}
+
+ private boolean hasAuxTasks() {
+ return configOut != null || templatesOut != null;
+ }
}
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/NBPackage.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/NBPackage.java
index 06ea784..63c9bf7 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/NBPackage.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/NBPackage.java
@@ -204,6 +204,34 @@
}
/**
+ * Copy templates to the provided destination. If a package type is
+ * specified in the provided configuration, only templates for that type
+ * will be copied. Template overrides in the configuration will be
+ * respected. The destination must be a directory or not already exist.
+ * Destination files must not already exist.
+ *
+ * @param configuration configuration to restrict / control templates
+ * @param destination directory to copy files in to (will be created if
+ * needed)
+ * @throws IOException if destination is not a directory, or cannot be
+ * created; if the template cannot be loaded; or if the destination file
+ * exists or cannot be created.
+ */
+ public static void copyTemplates(Configuration configuration, Path destination)
+ throws IOException {
+ Files.createDirectories(destination);
+ var type = configuration.getValue(PACKAGE_TYPE);
+ var templates = (type.isBlank() ? templates() : templates(type))
+ .toArray(Template[]::new);
+ for (var template : templates) {
+ var contents = template.load(configuration);
+ Files.writeString(destination.resolve(template.name()),
+ contents,
+ StandardOpenOption.CREATE_NEW);
+ }
+ }
+
+ /**
* Query all available packagers. Not all packagers are guaranteed to run on
* the current system / OS.
*
@@ -264,6 +292,26 @@
findPackager(packagerName).options());
}
+ /**
+ * Query all templates.
+ *
+ * @return stream of all templates
+ */
+ public static Stream<Template> templates() {
+ return packagers().flatMap(Packager::templates);
+ }
+
+ /**
+ * Query all templates used by the specified packager.
+ *
+ * @param packagerName name of packager
+ * @return stream of templates
+ * @throws IllegalArgumentException if no packager by this name exists
+ */
+ public static Stream<Template> templates(String packagerName) {
+ return findPackager(packagerName).templates();
+ }
+
// @TODO properly escape and support multi-line comments / values
private static void writeOption(StringBuilder sb, Configuration conf, Option<?> option, boolean comment) {
String value = conf.getValue(option);
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Option.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Option.java
index ffaf12b..d34122a 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Option.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Option.java
@@ -98,6 +98,18 @@
}
/**
+ * Create an Option of a String type with empty default value.
+ *
+ * @param key key used to store option in configuration
+ * @param comment help text for user
+ * @return option
+ */
+ public static Option<String> ofString(String key,
+ String comment) {
+ return ofString(key, "", comment);
+ }
+
+ /**
* Create an Option of a String type.
*
* @param key key used to store option in configuration
@@ -112,6 +124,18 @@
}
/**
+ * Create an option of a Path type with empty default value.
+ *
+ * @param key key used to store option in configuration
+ * @param comment help text for user
+ * @return option
+ */
+ public static Option<Path> ofPath(String key,
+ String comment) {
+ return ofPath(key, "", comment);
+ }
+
+ /**
* Create an option of a Path type.
*
* @param key key used to store option in configuration
@@ -146,7 +170,7 @@
/**
* Parser to convert text value into option type.
- *
+ *
* @param <T> option type
*/
@FunctionalInterface
@@ -154,7 +178,7 @@
/**
* Parse text to type.
- *
+ *
* @param text to parse
* @return value as type
* @throws Exception on parsing error
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Packager.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Packager.java
index d610cb4..a462749 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Packager.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Packager.java
@@ -53,6 +53,16 @@
}
/**
+ * A stream of packager-specific templates used by this packager. The
+ * default implementation provides an empty stream.
+ *
+ * @return stream of packager-specific templates
+ */
+ public default Stream<Template> templates() {
+ return Stream.empty();
+ }
+
+ /**
* Task API. A task only supports a single execution. Not all stages of the
* task may be executed - eg. when only creating a package image or creating
* a package from an image. The validation methods for all required stages
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/StringUtils.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/StringUtils.java
index 2dc052e..5844792 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/StringUtils.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/StringUtils.java
@@ -25,7 +25,7 @@
import java.util.regex.Pattern;
/**
- * A range of useful file utility functions for packagers.
+ * A range of useful String utility functions for packagers.
*/
public class StringUtils {
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Template.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Template.java
new file mode 100644
index 0000000..56d53f5
--- /dev/null
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/Template.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.netbeans.nbpackage;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.function.Supplier;
+
+/**
+ * Definition for a text template supported by NBPackage or one of the
+ * underlying packagers. Text will be loaded from the provided path option if
+ * set in the configuration, or from the default stream provided.
+ */
+public final class Template {
+
+ private final Option<Path> option;
+ private final String name;
+ private final Supplier<Reader> readerProvider;
+
+ private Template(Option<Path> option, String name, Supplier<Reader> readerSupplier) {
+ this.name = name;
+ this.option = option;
+ this.readerProvider = readerSupplier;
+ }
+
+ /**
+ * Name of the template. May be used for a file name for exporting
+ * templates.
+ *
+ * @return template name
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * The path option for overriding the default template.
+ *
+ * @return path option
+ */
+ public Option<Path> option() {
+ return option;
+ }
+
+ /**
+ * Load the text template. The path option from the provided
+ * {@link ExecutionContext} will be used if set, or the template will be
+ * loaded from the default source.
+ *
+ * @param context execution context for path option
+ * @return loaded template
+ * @throws IOException
+ */
+ public String load(ExecutionContext context) throws IOException {
+ Path file = context.getValue(option).orElse(null);
+ return loadImpl(file);
+ }
+
+ String load(Configuration config) throws IOException {
+ String optionValue = config.getValue(option);
+ if (!optionValue.isBlank()) {
+ return loadImpl(Path.of(optionValue));
+ } else {
+ return loadImpl(null);
+ }
+ }
+
+ private String loadImpl(Path file) throws IOException {
+ if (file != null) {
+ return Files.readString(file);
+ } else {
+ try (Reader in = readerProvider.get(); StringWriter out = new StringWriter()) {
+ in.transferTo(out);
+ return out.toString();
+ }
+ }
+ }
+
+ /**
+ * Create a template definition from the provided path option and default
+ * template source. The input stream for the default source should be
+ * readable as UTF-8 text. A packager will usually use
+ * {@link Class#getResourceAsStream(java.lang.String)}. The template name
+ * may be used as a file name for exporting templates.
+ *
+ * @param option user configurable path option to override template
+ * @param name template name / export file name
+ * @param defaultSourceSupplier supplier of input stream for default template
+ * @return template
+ */
+ public static Template of(Option<Path> option, String name,
+ Supplier<InputStream> defaultSourceSupplier) {
+ return new Template(option, name, ()
+ -> new InputStreamReader(defaultSourceSupplier.get(), StandardCharsets.UTF_8));
+ }
+
+}
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/appimage/AppImagePackager.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/appimage/AppImagePackager.java
index c9992f5..73f1fa6 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/appimage/AppImagePackager.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/appimage/AppImagePackager.java
@@ -25,6 +25,7 @@
import org.apache.netbeans.nbpackage.ExecutionContext;
import org.apache.netbeans.nbpackage.Option;
import org.apache.netbeans.nbpackage.Packager;
+import org.apache.netbeans.nbpackage.Template;
/**
* Packager for Linux AppImage, using appimagetool.
@@ -37,21 +38,21 @@
/**
* Path to appimagetool executable.
*/
- public static final Option<Path> APPIMAGE_TOOL
+ static final Option<Path> APPIMAGE_TOOL
= Option.ofPath("package.appimage.tool", "",
MESSAGES.getString("option.appimagetool.description"));
/**
* Path to png icon (48x48) as required by AppDir / XDG specification.
*/
- public static final Option<Path> APPIMAGE_ICON
+ static final Option<Path> APPIMAGE_ICON
= Option.ofPath("package.appimage.icon", "",
MESSAGES.getString("option.appimageicon.description"));
/**
* Category (or categories) to set in .desktop file.
*/
- public static final Option<String> APPIMAGE_CATEGORY
+ static final Option<String> APPIMAGE_CATEGORY
= Option.ofString("package.appimage.category",
"Development;Java;IDE;",
MESSAGES.getString("option.appimagecategory.description"));
@@ -60,16 +61,45 @@
* Architecture of AppImage to create. Defaults to parsing from appimagetool
* file name.
*/
- public static final Option<String> APPIMAGE_ARCH
+ static final Option<String> APPIMAGE_ARCH
= Option.ofString("package.appimage.arch",
"",
MESSAGES.getString("option.appimagearch.description"));
+
+ /**
+ * Optional path to custom .desktop template.
+ */
+ static final Option<Path> DESKTOP_TEMPLATE_PATH
+ = Option.ofPath("package.appimage.desktop-template",
+ MESSAGES.getString("option.desktop_template.description"));
-// public static final Option<Path> APPIMAGE_SVG_ICON
-// = Option.ofPath("package.appimage.svg", "",
-// MESSAGES.getString("option.appimagesvg.description"));
+ /**
+ * Desktop file template.
+ */
+ static final Template DESKTOP_TEMPLATE
+ = Template.of(DESKTOP_TEMPLATE_PATH, "AppImage.desktop.template",
+ () -> AppImagePackager.class.getResourceAsStream("AppImage.desktop.template"));
+
+ /**
+ * Optional path to custom AppRun launcher template.
+ */
+ static final Option<Path> LAUNCHER_TEMPLATE_PATH
+ = Option.ofPath("package.appimage.launcher-template",
+ MESSAGES.getString("option.launcher_template.description"));
+
+ /**
+ * AppRun launcher script template.
+ */
+ static final Template LAUNCHER_TEMPLATE
+ = Template.of(LAUNCHER_TEMPLATE_PATH, "AppImage.launcher.template",
+ () -> AppImagePackager.class.getResourceAsStream("AppImage.launcher.template"));
+
private static final List<Option<?>> APPIMAGE_OPTIONS
- = List.of(APPIMAGE_TOOL, APPIMAGE_ICON, APPIMAGE_CATEGORY, APPIMAGE_ARCH);
+ = List.of(APPIMAGE_TOOL, APPIMAGE_ICON, APPIMAGE_CATEGORY,
+ APPIMAGE_ARCH, DESKTOP_TEMPLATE_PATH, LAUNCHER_TEMPLATE_PATH);
+
+ private static final List<Template> APPIMAGE_TEMPLATES
+ = List.of(DESKTOP_TEMPLATE, LAUNCHER_TEMPLATE);
@Override
public Task createTask(ExecutionContext context) {
@@ -86,4 +116,9 @@
return APPIMAGE_OPTIONS.stream();
}
+ @Override
+ public Stream<Template> templates() {
+ return APPIMAGE_TEMPLATES.stream();
+ }
+
}
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/appimage/AppImageTask.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/appimage/AppImageTask.java
index e4e5c5d..dcd0b59 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/appimage/AppImageTask.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/appimage/AppImageTask.java
@@ -153,12 +153,7 @@
}
private void setupDesktopFile(Path image, String execName) throws IOException {
- String template;
- try ( var reader = new BufferedReader(
- new InputStreamReader(
- getClass().getResourceAsStream("AppImage.desktop.template")))) {
- template = reader.lines().collect(Collectors.joining("\n", "", "\n"));
- }
+ String template = AppImagePackager.DESKTOP_TEMPLATE.load(context());
String desktop = StringUtils.replaceTokens(template,
key -> "EXEC".equals(key) ? execName : context().tokenReplacementFor(key));
Path desktopDir = image.resolve("usr")
@@ -172,12 +167,7 @@
}
private void setupAppRunScript(Path image, String execName) throws IOException {
- String template;
- try ( var reader = new BufferedReader(
- new InputStreamReader(
- getClass().getResourceAsStream("AppImage.launcher.template")))) {
- template = reader.lines().collect(Collectors.joining("\n", "", "\n"));
- }
+ String template = AppImagePackager.LAUNCHER_TEMPLATE.load(context());
String appRun = StringUtils.replaceTokens(template, Map.of("EXEC", execName));
Path appRunPath = image.resolve("AppRun");
Files.writeString(appRunPath, appRun, StandardOpenOption.CREATE_NEW);
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/deb/DebPackager.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/deb/DebPackager.java
index ba1846c..2ff7569 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/deb/DebPackager.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/deb/DebPackager.java
@@ -25,6 +25,7 @@
import org.apache.netbeans.nbpackage.ExecutionContext;
import org.apache.netbeans.nbpackage.Option;
import org.apache.netbeans.nbpackage.Packager;
+import org.apache.netbeans.nbpackage.Template;
/**
* Packager for Linux DEB using dpkg-deb.
@@ -38,7 +39,7 @@
* Path to png icon (48x48) as required by AppDir / XDG specification.
* Defaults to Apache NetBeans icon.
*/
- public static final Option<Path> DEB_ICON
+ static final Option<Path> ICON_PATH
= Option.ofPath("package.deb.icon", "",
MESSAGES.getString("option.icon.description"));
@@ -46,7 +47,7 @@
* Path to svg icon. Will only be used if DEB_ICON is also set. Defaults to
* Apache NetBeans icon.
*/
- public static final Option<Path> DEB_SVG
+ static final Option<Path> SVG_ICON_PATH
= Option.ofPath("package.deb.svg-icon", "",
MESSAGES.getString("option.svg.description"));
@@ -54,44 +55,90 @@
* Name for the .desktop file (without suffix). Defaults to sanitized
* version of package name.
*/
- public static final Option<String> DEB_DESKTOP_FILENAME
+ static final Option<String> DESKTOP_FILENAME
= Option.ofString("package.deb.desktop-filename", "",
MESSAGES.getString("option.desktopfilename.description"));
/**
* StartupWMClass to set in .desktop file.
*/
- public static final Option<String> DEB_WMCLASS
+ static final Option<String> DESKTOP_WMCLASS
= Option.ofString("package.deb.wmclass",
"${package.name}",
MESSAGES.getString("option.wmclass.description"));
-
+
/**
* Category (or categories) to set in .desktop file.
*/
- public static final Option<String> DEB_CATEGORY
+ static final Option<String> DESKTOP_CATEGORY
= Option.ofString("package.deb.category",
"Development;Java;IDE;",
MESSAGES.getString("option.category.description"));
-
+
/**
* Maintainer name and email for Debian Control file.
*/
- public static final Option<String> DEB_MAINTAINER
+ static final Option<String> DEB_MAINTAINER
= Option.ofString("package.deb.maintainer", "",
MESSAGES.getString("option.maintainer.description"));
-
+
/**
* Package description for Debian Control file.
*/
- public static final Option<String> DEB_DESCRIPTION
- = Option.ofString("package.deb.description",
+ static final Option<String> DEB_DESCRIPTION
+ = Option.ofString("package.deb.description",
"Package of ${package.name} ${package.version}.",
MESSAGES.getString("option.description.description"));
+ /**
+ * Optional path to custom DEB control template.
+ */
+ static final Option<Path> CONTROL_TEMPLATE_PATH
+ = Option.ofPath("package.deb.control-template",
+ MESSAGES.getString("option.control_template.description"));
+
+ /**
+ * DEB control template.
+ */
+ static final Template CONTROL_TEMPLATE
+ = Template.of(CONTROL_TEMPLATE_PATH, "deb.control.template",
+ () -> DebPackager.class.getResourceAsStream("deb.control.template"));
+
+ /**
+ * Optional path to custom .desktop template.
+ */
+ static final Option<Path> DESKTOP_TEMPLATE_PATH
+ = Option.ofPath("package.deb.desktop-template",
+ MESSAGES.getString("option.desktop_template.description"));
+
+ /**
+ * Desktop file template.
+ */
+ static final Template DESKTOP_TEMPLATE
+ = Template.of(DESKTOP_TEMPLATE_PATH, "deb.desktop.template",
+ () -> DebPackager.class.getResourceAsStream("deb.desktop.template"));
+
+ /**
+ * Optional path to custom launcher template.
+ */
+ static final Option<Path> LAUNCHER_TEMPLATE_PATH
+ = Option.ofPath("package.deb.launcher-template",
+ MESSAGES.getString("option.launcher_template.description"));
+
+ /**
+ * Launcher script template.
+ */
+ static final Template LAUNCHER_TEMPLATE
+ = Template.of(LAUNCHER_TEMPLATE_PATH, "deb.launcher.template",
+ () -> DebPackager.class.getResourceAsStream("deb.launcher.template"));
+
private static final List<Option<?>> DEB_OPTIONS
- = List.of(DEB_ICON, DEB_SVG, DEB_DESKTOP_FILENAME, DEB_WMCLASS,
- DEB_CATEGORY, DEB_MAINTAINER, DEB_DESCRIPTION);
+ = List.of(ICON_PATH, SVG_ICON_PATH, DESKTOP_FILENAME, DESKTOP_WMCLASS,
+ DESKTOP_CATEGORY, DEB_MAINTAINER, DEB_DESCRIPTION,
+ CONTROL_TEMPLATE_PATH, DESKTOP_TEMPLATE_PATH, LAUNCHER_TEMPLATE_PATH);
+
+ private static final List<Template> DEB_TEMPLATES
+ = List.of(CONTROL_TEMPLATE, DESKTOP_TEMPLATE, LAUNCHER_TEMPLATE);
@Override
public Task createTask(ExecutionContext context) {
@@ -108,4 +155,9 @@
return DEB_OPTIONS.stream();
}
+ @Override
+ public Stream<Template> templates() {
+ return DEB_TEMPLATES.stream();
+ }
+
}
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/deb/DebTask.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/deb/DebTask.java
index eb7127b..006c8ca 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/deb/DebTask.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/deb/DebTask.java
@@ -18,9 +18,7 @@
*/
package org.apache.netbeans.nbpackage.deb;
-import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStreamReader;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -31,7 +29,6 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.stream.Collectors;
import org.apache.netbeans.nbpackage.AbstractPackagerTask;
import org.apache.netbeans.nbpackage.ExecutionContext;
import org.apache.netbeans.nbpackage.NBPackage;
@@ -188,12 +185,7 @@
private void setupLauncher(Path binDir, String packageLocation, String execName)
throws IOException {
- String template;
- try ( var reader = new BufferedReader(
- new InputStreamReader(
- getClass().getResourceAsStream("deb.launcher.template")))) {
- template = reader.lines().collect(Collectors.joining("\n", "", "\n"));
- }
+ String template = DebPackager.LAUNCHER_TEMPLATE.load(context());
String script = StringUtils.replaceTokens(template,
Map.of("PACKAGE", packageLocation, "EXEC", execName));
Path bin = binDir.resolve(execName);
@@ -210,8 +202,8 @@
.resolve("hicolor")
.resolve("scalable")
.resolve("apps");
- Path icon = context().getValue(DebPackager.DEB_ICON).orElse(null);
- Path svg = context().getValue(DebPackager.DEB_SVG).orElse(null);
+ Path icon = context().getValue(DebPackager.ICON_PATH).orElse(null);
+ Path svg = context().getValue(DebPackager.SVG_ICON_PATH).orElse(null);
if (svg != null && icon == null) {
context().warningHandler().accept(DebPackager.MESSAGES.getString("message.svgnoicon"));
svg = null;
@@ -236,12 +228,7 @@
}
private void setupDesktopFile(Path share, String exec, String pkgName) throws IOException {
- String template;
- try ( var reader = new BufferedReader(
- new InputStreamReader(
- getClass().getResourceAsStream("deb.desktop.template")))) {
- template = reader.lines().collect(Collectors.joining("\n", "", "\n"));
- }
+ String template = DebPackager.DESKTOP_TEMPLATE.load(context());
Map<String, String> tokens = Map.of("EXEC", exec, "ICON", pkgName);
String desktop = StringUtils.replaceTokens(template,
key -> {
@@ -254,7 +241,7 @@
});
Path desktopDir = share.resolve("applications");
Files.createDirectories(desktopDir);
- String desktopFileName = context().getValue(DebPackager.DEB_DESKTOP_FILENAME)
+ String desktopFileName = context().getValue(DebPackager.DESKTOP_FILENAME)
.map(name -> sanitize(name))
.orElse(pkgName);
Path desktopFile = desktopDir.resolve(desktopFileName + ".desktop");
@@ -262,12 +249,7 @@
}
private void setupControlFile(Path DEBIAN) throws Exception {
- String template;
- try ( var reader = new BufferedReader(
- new InputStreamReader(
- getClass().getResourceAsStream("deb.control.template")))) {
- template = reader.lines().collect(Collectors.joining("\n", "", "\n"));
- }
+ String template = DebPackager.CONTROL_TEMPLATE.load(context());
String maintainer = context().getValue(DebPackager.DEB_MAINTAINER)
.orElse("");
if (maintainer.isBlank()) {
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/innosetup/InnoSetupPackager.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/innosetup/InnoSetupPackager.java
index bec5933..9d99e09 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/innosetup/InnoSetupPackager.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/innosetup/InnoSetupPackager.java
@@ -25,6 +25,7 @@
import org.apache.netbeans.nbpackage.ExecutionContext;
import org.apache.netbeans.nbpackage.Option;
import org.apache.netbeans.nbpackage.Packager;
+import org.apache.netbeans.nbpackage.Template;
/**
* Packager for Windows .exe installer using Inno Setup.
@@ -38,21 +39,21 @@
* Path to InnoSetup compiler executable. Or to Linux script to invoke via
* Wine.
*/
- public static final Option<Path> INNOSETUP_TOOL
+ static final Option<Path> TOOL_PATH
= Option.ofPath("package.innosetup.tool", "",
MESSAGES.getString("option.innosetuptool.description"));
/**
* InnoSetup App ID.
*/
- public static final Option<String> INNOSETUP_APPID
+ static final Option<String> APPID
= Option.ofString("package.innosetup.appid", "",
MESSAGES.getString("option.innosetupappid.description"));
/**
* Path to icon file (*.ico).
*/
- public static final Option<Path> INNOSETUP_ICON
+ static final Option<Path> ICON_PATH
= Option.ofPath("package.innosetup.icon", "",
MESSAGES.getString("option.innosetupicon.description"));
@@ -60,20 +61,30 @@
* Path to optional license file (*.txt or *.rtf) to display during
* installation.
*/
- public static final Option<Path> INNOSETUP_LICENSE
+ static final Option<Path> LICENSE_PATH
= Option.ofPath("package.innosetup.license", "",
MESSAGES.getString("option.innosetuplicense.description"));
/**
* Path to alternative InnoSetup template.
*/
- public static final Option<Path> INNOSETUP_TEMPLATE
+ static final Option<Path> ISS_TEMPLATE_PATH
= Option.ofPath("package.innosetup.template", "",
MESSAGES.getString("option.innosetuptemplate.description"));
+
+ /**
+ * ISS file template.
+ */
+ static final Template ISS_TEMPLATE
+ = Template.of(ISS_TEMPLATE_PATH, "InnoSetup.iss.template",
+ () -> InnoSetupPackager.class.getResourceAsStream("InnoSetup.iss.template"));
private static final List<Option<?>> INNOSETUP_OPTIONS
- = List.of(INNOSETUP_TOOL, INNOSETUP_APPID, INNOSETUP_ICON,
- INNOSETUP_LICENSE, INNOSETUP_TEMPLATE);
+ = List.of(TOOL_PATH, APPID, ICON_PATH,
+ LICENSE_PATH, ISS_TEMPLATE_PATH);
+
+ private static final List<Template> INNOSETUP_TEMPLATES
+ = List.of(ISS_TEMPLATE);
@Override
public Task createTask(ExecutionContext context) {
@@ -90,4 +101,9 @@
return INNOSETUP_OPTIONS.stream();
}
+ @Override
+ public Stream<Template> templates() {
+ return INNOSETUP_TEMPLATES.stream();
+ }
+
}
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/innosetup/InnoSetupTask.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/innosetup/InnoSetupTask.java
index 74cca5c..3679559 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/innosetup/InnoSetupTask.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/innosetup/InnoSetupTask.java
@@ -43,7 +43,7 @@
@Override
public void validateCreatePackage() throws Exception {
- context().getValue(INNOSETUP_TOOL)
+ context().getValue(TOOL_PATH)
.orElseThrow(() -> new IllegalStateException(
MESSAGES.getString("message.noinnosetuptool")));
}
@@ -65,7 +65,7 @@
@Override
public Path createPackage(Path image) throws Exception {
- Path tool = context().getValue(INNOSETUP_TOOL)
+ Path tool = context().getValue(TOOL_PATH)
.orElseThrow(() -> new IllegalStateException(
MESSAGES.getString("message.noinnosetuptool")))
.toAbsolutePath();
@@ -126,7 +126,7 @@
}
private void setupIcons(Path image, String execName) throws IOException {
- Path icoFile = context().getValue(INNOSETUP_ICON).orElse(null);
+ Path icoFile = context().getValue(ICON_PATH).orElse(null);
Path dstFile = image.resolve(execName)
.resolve("etc")
.resolve(execName + ".ico");
@@ -141,7 +141,7 @@
}
private void setupLicenseFile(Path image) throws IOException {
- var license = context().getValue(INNOSETUP_LICENSE).orElse(null);
+ var license = context().getValue(LICENSE_PATH).orElse(null);
if (license == null) {
return;
}
@@ -156,15 +156,9 @@
}
private void createInnoSetupScript(Path image, String execName) throws IOException {
- Path templateFile = context().getValue(INNOSETUP_TEMPLATE).orElse(null);
- String template;
- try (var reader = templateFile != null
- ? Files.newBufferedReader(templateFile)
- : new BufferedReader(
- new InputStreamReader(
- getClass().getResourceAsStream("InnoSetup.iss.template")))) {
- template = reader.lines().collect(Collectors.joining("\r\n", "", "\r\n"));
- }
+ // make sure loaded template has correct line endings
+ String template = ISS_TEMPLATE.load(context()).lines()
+ .collect(Collectors.joining("\r\n", "", "\r\n"));
List<Path> files;
try (var l = Files.list(image.resolve(execName))) {
@@ -176,7 +170,7 @@
String appName = context().getValue(NBPackage.PACKAGE_NAME).orElse(execName);
String appNameSafe = sanitize(appName);
- String appID = context().getValue(INNOSETUP_APPID).orElse(appName);
+ String appID = context().getValue(APPID).orElse(appName);
String appVersion = context().getValue(NBPackage.PACKAGE_VERSION).orElse("1.0");
String appLicense;
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/AppBundleTask.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/AppBundleTask.java
index 77d0924..aa70f6d 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/AppBundleTask.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/AppBundleTask.java
@@ -18,71 +18,104 @@
*/
package org.apache.netbeans.nbpackage.macos;
-import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.netbeans.nbpackage.AbstractPackagerTask;
import org.apache.netbeans.nbpackage.ExecutionContext;
+import org.apache.netbeans.nbpackage.FileUtils;
import org.apache.netbeans.nbpackage.NBPackage;
import org.apache.netbeans.nbpackage.StringUtils;
-import static org.apache.netbeans.nbpackage.macos.PkgPackager.*;
-
/**
*
*/
class AppBundleTask extends AbstractPackagerTask {
-
+
+ private static final String DEFAULT_JAR_INTERNAL_BIN_GLOB = "**/*.{dylib,jnilib}";
+ private static final String NATIVE_BIN_FILENAME = "nativeBinaries";
+ private static final String JAR_BIN_FILENAME = "jarBinaries";
+ private static final String ENTITLEMENTS_FILENAME = "sandbox.plist";
+ private static final String LAUNCHER_SRC_DIRNAME = "macos-launcher-src";
+
private String bundleName;
-
+
AppBundleTask(ExecutionContext context) {
super(context);
}
-
+
@Override
public void validateCreatePackage() throws Exception {
- throw new UnsupportedOperationException("Not supported yet.");
+ String[] cmds;
+ if (context().getValue(MacOS.CODESIGN_ID).isEmpty()) {
+ cmds = new String[] {"swift"};
+ } else {
+ cmds = new String[] {"swift", "codesign"};
+ }
+ validateTools(cmds);
}
-
+
@Override
public Path createImage(Path input) throws Exception {
Path image = super.createImage(input);
Path bundle = image.resolve(getBundleName() + ".app");
Path contents = bundle.resolve("Contents");
Path resources = contents.resolve("Resources");
-
- String execName = findLauncher(
- resources
- .resolve("APPDIR")
- .resolve("bin"))
+
+ String execName = findLauncher(resources.resolve("APPDIR").resolve("bin"))
.getFileName().toString();
Files.move(resources.resolve("APPDIR"), resources.resolve(execName));
-
+
Files.createDirectory(contents.resolve("MacOS"));
setupIcons(resources, execName);
setupInfo(contents, execName);
setupLauncherSource(image);
-
+ setupSigningConfiguration(image, bundle);
+
return image;
-
+
}
-
+
@Override
public Path createPackage(Path image) throws Exception {
- throw new UnsupportedOperationException("Not supported yet.");
+ Path bundle = image.resolve(getBundleName() + ".app");
+
+ String execName = FileUtils.find(bundle, "Contents/Resources/*/bin/*")
+ .stream()
+ .filter(path -> !path.toString().endsWith(".exe"))
+ .findFirst()
+ .map(path -> path.getFileName().toString())
+ .orElseThrow();
+ Path launcher = compileLauncher(image.resolve(LAUNCHER_SRC_DIRNAME));
+ Files.copy(launcher, bundle.resolve("Contents")
+ .resolve("MacOS").resolve(execName),
+ StandardCopyOption.COPY_ATTRIBUTES);
+
+ String signID = context().getValue(MacOS.CODESIGN_ID).orElse("");
+ if (signID.isBlank()) {
+ context().warningHandler().accept(
+ MacOS.MESSAGES.getString("message.nocodesignid"));
+ return bundle;
+ }
+ Path entitlements = image.resolve(ENTITLEMENTS_FILENAME);
+ signBinariesInJARs(image, entitlements, signID);
+ signNativeBinaries(image, entitlements, signID);
+ codesign(bundle, entitlements, signID);
+ return bundle;
}
-
+
@Override
protected String imageName(Path input) throws Exception {
return super.imageName(input) + "-macOS-app";
}
-
+
@Override
protected Path applicationDirectory(Path image) throws Exception {
return image.resolve(getBundleName() + ".app")
@@ -90,14 +123,14 @@
.resolve("Resources")
.resolve("APPDIR");
}
-
+
@Override
protected Path runtimeDirectory(Path image, Path application) throws Exception {
return image.resolve(getBundleName() + ".app")
.resolve("Contents")
.resolve("Home");
}
-
+
String getBundleName() {
if (bundleName == null) {
var name = sanitize(context().getValue(NBPackage.PACKAGE_NAME).orElseThrow());
@@ -108,24 +141,39 @@
}
return bundleName;
}
-
- private String sanitize(String name) {
+
+ void validateTools(String... tools) throws Exception {
+ if (context().isVerbose()) {
+ context().infoHandler().accept(MessageFormat.format(
+ MacOS.MESSAGES.getString("message.validatingtools"),
+ Arrays.toString(tools)));
+ }
+ for (String tool : tools) {
+ if (context().exec(List.of("which", tool)) != 0) {
+ throw new IllegalStateException(MessageFormat.format(
+ MacOS.MESSAGES.getString("message.missingtool"),
+ tool));
+ }
+ }
+ }
+
+ String sanitize(String name) {
return name.replaceAll("[\\\\/:*?\"<>|]", "_");
}
-
+
private String sanitizeBundleID(String name) {
return name.replaceAll("[^a-zA-Z0-9-\\.]", "-");
}
-
+
private Path findLauncher(Path binDir) throws IOException {
try ( var files = Files.list(binDir)) {
return files.filter(f -> !f.getFileName().toString().endsWith(".exe"))
.findFirst().orElseThrow(IOException::new);
}
}
-
+
private void setupIcons(Path resources, String execName) throws IOException {
- Path icnsFile = context().getValue(MACOS_ICON).orElse(null);
+ Path icnsFile = context().getValue(MacOS.ICON_PATH).orElse(null);
Path dstFile = resources.resolve(execName + ".icns");
if (icnsFile != null) {
Files.copy(icnsFile, dstFile);
@@ -134,64 +182,129 @@
"/org/apache/netbeans/nbpackage/apache-netbeans.icns"), dstFile);
}
}
-
+
private void setupInfo(Path contents, String execName) throws IOException {
- Path templateFile = context().getValue(MACOS_INFO_TEMPLATE).orElse(null);
- String template;
- try ( var reader = templateFile != null
- ? Files.newBufferedReader(templateFile)
- : new BufferedReader(
- new InputStreamReader(
- getClass().getResourceAsStream("Info.plist.template"),
- StandardCharsets.UTF_8))) {
- template = reader.lines().collect(Collectors.joining("\n", "", "\n"));
- }
-
+ String template = MacOS.INFO_TEMPLATE.load(context());
+
var tokenMap = Map.of(
"BUNDLE_NAME", getBundleName(),
"BUNDLE_DISPLAY", context().getValue(NBPackage.PACKAGE_NAME).orElseThrow(),
"BUNDLE_VERSION", context().getValue(NBPackage.PACKAGE_VERSION).orElseThrow(),
"BUNDLE_EXEC", execName,
- "BUNDLE_ID", context().getValue(MACOS_BUNDLE_ID)
+ "BUNDLE_ID", context().getValue(MacOS.BUNDLE_ID)
.orElse(sanitizeBundleID(getBundleName())),
"BUNDLE_ICON", execName + ".icns"
);
-
+
String info = StringUtils.replaceTokens(template, tokenMap);
-
+
Files.writeString(contents.resolve("Info.plist"), info,
StandardOpenOption.CREATE_NEW);
-
+
}
-
+
private void setupLauncherSource(Path image) throws IOException {
- Path launcherProject = image.resolve("macos-launcher-src");
+ Path launcherProject = image.resolve(LAUNCHER_SRC_DIRNAME);
Files.createDirectories(launcherProject);
Path sourceDir = launcherProject.resolve("Sources").resolve("AppLauncher");
Files.createDirectories(sourceDir);
-
- String packageSwift;
- try ( var reader = new BufferedReader(
- new InputStreamReader(
- getClass().getResourceAsStream("Package.swift.template"),
- StandardCharsets.UTF_8
- ))) {
- packageSwift = reader.lines().collect(Collectors.joining("\n", "", "\n"));
- }
-
- String mainSwift;
- try ( var reader = new BufferedReader(
- new InputStreamReader(
- getClass().getResourceAsStream("main.swift.template"),
- StandardCharsets.UTF_8
- ))) {
- mainSwift = reader.lines().collect(Collectors.joining("\n", "", "\n"));
- }
-
+
+ String packageSwift = MacOS.LAUNCHER_PACKAGE_TEMPLATE.load(context());
+ String mainSwift = MacOS.LAUNCHER_TEMPLATE.load(context());
+
Files.writeString(launcherProject.resolve("Package.swift"),
packageSwift, StandardOpenOption.CREATE_NEW);
Files.writeString(sourceDir.resolve("main.swift"),
mainSwift, StandardOpenOption.CREATE_NEW);
}
+
+ private void setupSigningConfiguration(Path image, Path bundle) throws IOException {
+ Files.writeString(image.resolve(ENTITLEMENTS_FILENAME),
+ MacOS.ENTITLEMENTS_TEMPLATE.load(context())
+ , StandardOpenOption.CREATE_NEW);
+ var nativeBinaries = FileUtils.find(bundle,
+ context().getValue(MacOS.SIGNING_FILES).orElseThrow());
+ Files.writeString(image.resolve(NATIVE_BIN_FILENAME),
+ nativeBinaries.stream()
+ .map(path -> image.relativize(path))
+ .map(Path::toString)
+ .collect(Collectors.joining("\n", "", "\n")),
+ StandardOpenOption.CREATE_NEW);
+ var jarBinaries = FileUtils.find(bundle,
+ context().getValue(MacOS.SIGNING_JARS).orElseThrow());
+ Files.writeString(image.resolve(JAR_BIN_FILENAME),
+ jarBinaries.stream()
+ .map(path -> image.relativize(path))
+ .map(Path::toString)
+ .collect(Collectors.joining("\n", "", "\n")),
+ StandardOpenOption.CREATE_NEW);
+ }
+
+ private Path compileLauncher(Path launcherProject) throws IOException, InterruptedException {
+ var pb = new ProcessBuilder("swift", "build",
+ "--configuration", "release",
+ "--arch", "x86_64");
+ pb.directory(launcherProject.toFile());
+ context().exec(pb);
+ Path launcher = launcherProject.resolve(".build/release/AppLauncher");
+ if (!Files.exists(launcher)) {
+ throw new IOException(launcher.toString());
+ }
+ return launcher;
+ }
+
+ private void signBinariesInJARs(Path image, Path entitlements, String id)
+ throws IOException {
+ Path jarFiles = image.resolve(JAR_BIN_FILENAME);
+ if (!Files.exists(jarFiles)) {
+ return;
+ }
+ List<Path> jars = Files.readString(jarFiles).lines()
+ .filter(l -> !l.isBlank())
+ .map(Path::of)
+ .map(image::resolve)
+ .collect(Collectors.toList());
+ for (Path jar : jars) {
+ FileUtils.processJarContents(jar,
+ DEFAULT_JAR_INTERNAL_BIN_GLOB,
+ (file, path) -> {
+ codesign(file, entitlements, id);
+ return true;
+ }
+ );
+ }
+ }
+
+ private void signNativeBinaries(Path image, Path entitlements, String id)
+ throws IOException {
+ Path nativeFiles = image.resolve(NATIVE_BIN_FILENAME);
+ if (!Files.exists(nativeFiles)) {
+ return;
+ }
+ List<Path> files = Files.readString(nativeFiles).lines()
+ .filter(l -> !l.isBlank())
+ .map(Path::of)
+ .map(image::resolve)
+ .collect(Collectors.toList());
+ for (Path file : files) {
+ codesign(file, entitlements, id);
+ }
+ }
+ private void codesign(Path file, Path entitlements, String id)
+ throws IOException {
+ try {
+ context().exec("codesign",
+ "--force",
+ "--timestamp",
+ "--options=runtime",
+ "--entitlements", entitlements.toString(),
+ "-s", id,
+ "-v",
+ file.toString());
+ } catch (InterruptedException ex) {
+ throw new IOException(ex);
+ }
+ }
+
}
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/MacOS.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/MacOS.java
new file mode 100644
index 0000000..2ab0b15
--- /dev/null
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/MacOS.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.netbeans.nbpackage.macos;
+
+import java.nio.file.Path;
+import java.util.ResourceBundle;
+import org.apache.netbeans.nbpackage.Option;
+import org.apache.netbeans.nbpackage.Template;
+
+/**
+ * Package private options and utilities across packagers.
+ */
+class MacOS {
+
+ private static final String DEFAULT_BIN_GLOB = "{*.dylib,*.jnilib,**/nativeexecution/MacOSX-*/*,Contents/Home/bin/*,Contents/Home/lib/jspawnhelper}";
+ private static final String DEFAULT_JAR_BIN_GLOB = "{jna-5*.jar,junixsocket-native-common-*.jar,launcher-common-*.jar,jansi-*.jar,nbi-engine.jar}";
+
+ static final ResourceBundle MESSAGES
+ = ResourceBundle.getBundle(PkgPackager.class.getPackageName() + ".Messages");
+
+ /**
+ * Value for CFBundleIdentifier.
+ */
+ static final Option<String> BUNDLE_ID
+ = Option.ofString("package.macos.bundleid",
+ MESSAGES.getString("option.bundle_id.description"));
+
+ /**
+ * Path to icon (*.icns) file.
+ */
+ static final Option<Path> ICON_PATH
+ = Option.ofPath("package.macos.icon",
+ MESSAGES.getString("option.icon.description"));
+
+ /**
+ * Optional Info.plist template path.
+ */
+ static final Option<Path> INFO_TEMPLATE_PATH
+ = Option.ofPath("package.macos.info-template",
+ MESSAGES.getString("option.info_template.description"));
+
+ /**
+ * Info.plist template.
+ */
+ static final Template INFO_TEMPLATE
+ = Template.of(INFO_TEMPLATE_PATH, "Info.plist.template",
+ () -> MacOS.class.getResourceAsStream("Info.plist.template"));
+
+ /**
+ * Optional launcher (main.swift) template path.
+ */
+ static final Option<Path> LAUNCHER_TEMPLATE_PATH
+ = Option.ofPath("package.macos.launcher-template",
+ MESSAGES.getString("option.launcher_template.description"));
+
+ /**
+ * Launcher (main.swift) template.
+ */
+ static final Template LAUNCHER_TEMPLATE
+ = Template.of(LAUNCHER_TEMPLATE_PATH, "main.swift.template",
+ () -> MacOS.class.getResourceAsStream("main.swift.template"));
+
+ /**
+ * Unlisted Swift package template.
+ */
+ static final Option<Path> LAUNCHER_PACKAGE_TEMPLATE_PATH
+ = Option.ofPath("package.macos.launcher-package-template", "");
+
+ /**
+ * Unlisted launcher package (Package.swift) template.
+ */
+ static final Template LAUNCHER_PACKAGE_TEMPLATE
+ = Template.of(LAUNCHER_PACKAGE_TEMPLATE_PATH, "Package.swift.template",
+ () -> MacOS.class.getResourceAsStream("Package.swift.template"));
+
+ /**
+ * Optional codesign entitlements template path.
+ */
+ static final Option<Path> ENTITLEMENTS_TEMPLATE_PATH
+ = Option.ofPath("package.macos.entitlements-template",
+ MESSAGES.getString("option.entitlements_template.description"));
+
+ /**
+ * Codesign entitlements template.
+ */
+ static final Template ENTITLEMENTS_TEMPLATE
+ = Template.of(ENTITLEMENTS_TEMPLATE_PATH, "sandbox.plist.template",
+ () -> MacOS.class.getResourceAsStream("sandbox.plist.template"));
+
+ /**
+ * Search pattern for files that need to be code signed.
+ */
+ static final Option<String> SIGNING_FILES
+ = Option.ofString("package.macos.codesign-files", DEFAULT_BIN_GLOB,
+ MESSAGES.getString("option.codesign_files.description"));
+
+ /**
+ * Search pattern for JARs containing native binaries that need to be code
+ * signed.
+ */
+ static final Option<String> SIGNING_JARS
+ = Option.ofString("package.macos.codesign-jars", DEFAULT_JAR_BIN_GLOB,
+ MESSAGES.getString("option.codesign_jars.description"));
+
+ /**
+ * Codesign ID for signing binaries and app bundle.
+ */
+ static final Option<String> CODESIGN_ID
+ = Option.ofString("package.macos.codesign-id",
+ MESSAGES.getString("option.codesign_id.description"));
+
+ /**
+ * Pkgbuild ID for signing installer.
+ */
+ static final Option<String> PKGBUILD_ID
+ = Option.ofString("package.macos.pkgbuild-id",
+ MESSAGES.getString("option.pkgbuild_id.description"));
+
+ private MacOS() {
+ }
+
+}
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/PkgPackager.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/PkgPackager.java
index f8de2a8..2fe4ae7 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/PkgPackager.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/PkgPackager.java
@@ -18,46 +18,35 @@
*/
package org.apache.netbeans.nbpackage.macos;
-import java.nio.file.Path;
import java.util.List;
-import java.util.ResourceBundle;
import java.util.stream.Stream;
import org.apache.netbeans.nbpackage.ExecutionContext;
import org.apache.netbeans.nbpackage.Option;
import org.apache.netbeans.nbpackage.Packager;
+import org.apache.netbeans.nbpackage.Template;
/**
* Packager for macOS PKG installer.
*/
public class PkgPackager implements Packager {
-
- static final ResourceBundle MESSAGES
- = ResourceBundle.getBundle(PkgPackager.class.getPackageName() + ".Messages");
- /**
- * Value for CFBundleIdentifier.
- */
- public static final Option<String> MACOS_BUNDLE_ID
- = Option.ofString("package.macos.bundleid", "",
- MESSAGES.getString("option.bundle_id.description"));
-
- /**
- * Path to icon (*.icns) file.
- */
- public static final Option<Path> MACOS_ICON
- = Option.ofPath("package.macos.icon", "",
- MESSAGES.getString("option.icon.description"));
-
- /**
- * Optional Info.plist template.
- */
- public static final Option<Path> MACOS_INFO_TEMPLATE
- = Option.ofPath("package.macos.info-template", "",
- MESSAGES.getString("option.info_template.description"));
-
private static final List<Option<?>> PKG_OPTIONS = List.of(
- MACOS_BUNDLE_ID, MACOS_ICON);
-
+ MacOS.BUNDLE_ID,
+ MacOS.ICON_PATH,
+ MacOS.INFO_TEMPLATE_PATH,
+ MacOS.LAUNCHER_TEMPLATE_PATH,
+ MacOS.ENTITLEMENTS_TEMPLATE_PATH,
+ MacOS.SIGNING_FILES,
+ MacOS.SIGNING_JARS,
+ MacOS.CODESIGN_ID,
+ MacOS.PKGBUILD_ID);
+
+ private static final List<Template> PKG_TEMPLATE = List.of(
+ MacOS.INFO_TEMPLATE,
+ MacOS.LAUNCHER_TEMPLATE,
+ MacOS.ENTITLEMENTS_TEMPLATE
+ );
+
@Override
public Task createTask(ExecutionContext context) {
return new PkgTask(context);
@@ -73,4 +62,9 @@
return PKG_OPTIONS.stream();
}
+ @Override
+ public Stream<Template> templates() {
+ return PKG_TEMPLATE.stream();
+ }
+
}
diff --git a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/PkgTask.java b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/PkgTask.java
index 755fe37..4caa185 100644
--- a/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/PkgTask.java
+++ b/nbpackage/src/main/java/org/apache/netbeans/nbpackage/macos/PkgTask.java
@@ -19,7 +19,10 @@
package org.apache.netbeans.nbpackage.macos;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
import org.apache.netbeans.nbpackage.ExecutionContext;
+import org.apache.netbeans.nbpackage.NBPackage;
/**
*
@@ -29,15 +32,45 @@
PkgTask(ExecutionContext context) {
super(context);
}
-
+
@Override
- public Path createPackage(Path image) throws Exception {
- throw new UnsupportedOperationException("Not supported yet.");
+ public void validateCreatePackage() throws Exception {
+ super.validateCreatePackage();
+ validateTools("pkgbuild");
}
@Override
- public void validateCreatePackage() throws Exception {
- throw new UnsupportedOperationException("Not supported yet.");
+ public Path createPackage(Path image) throws Exception {
+ Path bundle = super.createPackage(image);
+ String name = context().getValue(NBPackage.PACKAGE_NAME).orElseThrow();
+ String version = context().getValue(NBPackage.PACKAGE_VERSION).orElseThrow();
+ Path output = context().destination().resolve(
+ sanitize(name) + " " + sanitize(version) + ".pkg");
+ String signingID = context().getValue(MacOS.PKGBUILD_ID).orElse("");
+ List<String> command = new ArrayList<>();
+ command.add("pkgbuild");
+ command.add("--component");
+ command.add(bundle.toString());
+ command.add("--version");
+ command.add(version);
+ command.add("--install-location");
+ command.add("/Applications");
+
+ if (signingID.isBlank()) {
+ context().warningHandler().accept(
+ MacOS.MESSAGES.getString("message.nopkgbuildid"));
+ } else {
+ command.add("--sign");
+ command.add(signingID);
+ }
+
+ command.add(output.toString());
+ int result = context().exec(command);
+ if (result != 0) {
+ throw new Exception();
+ } else {
+ return output;
+ }
}
@Override
diff --git a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/Messages.properties b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/Messages.properties
index 272084f..961d630 100644
--- a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/Messages.properties
+++ b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/Messages.properties
@@ -24,6 +24,7 @@
option.output.description=Output directory. Defaults to current working directory. Must exist.
option.config.description=Path to configuration file to load.
option.saveconfig.description=Path to save configuration for editing. Must not exist.
+option.savetemplates.description=Path to save templates for editing. Must not exist or be an existing directory.
option.imageonly.description=Output package image only (advanced).
option.verbose.description=Output extra diagnostic information during execution.
option.property.description=Override the value of configuration options - eg. -Ppackage.version=2.2.
@@ -47,4 +48,4 @@
message.validatingpackage=Checking package creation prerequisites.
message.creatingimage=Building image from {0}.
message.creatingpackage=Building package from {0}.
-message.outputcreated=Successfully built {0}.
\ No newline at end of file
+message.outputcreated=Successfully built {0}.
diff --git a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/appimage/AppImage.desktop.template b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/appimage/AppImage.desktop.template
index 6fe22a7..313b400 100644
--- a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/appimage/AppImage.desktop.template
+++ b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/appimage/AppImage.desktop.template
@@ -1,6 +1,6 @@
[Desktop Entry]
Encoding=UTF-8
-Name=${package.name} ${package.version}
+Name=${package.name}
Exec=${EXEC}
Icon=${EXEC}
Categories=${package.appimage.category}
diff --git a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/appimage/Messages.properties b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/appimage/Messages.properties
index 97ce573..657d25c 100644
--- a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/appimage/Messages.properties
+++ b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/appimage/Messages.properties
@@ -19,5 +19,7 @@
option.appimageicon.description=Path to 48x48 png icon as required by xdg specification. Defaults to Apache NetBeans logo.
option.appimagecategory.description=Application category (or categories) to use in the AppImage .desktop file.
option.appimagearch.description=Architecture to build appimage for. By default will extract from appimagetool name.
+option.desktop_template.description=Optional path to custom .desktop file template.
+option.launcher_template.description=Optional path to custom AppRun launcher script template.
message.noappimagetool=The package.appimage.tool option must be set.
\ No newline at end of file
diff --git a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/deb/Messages.properties b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/deb/Messages.properties
index b392429..a1ab98d 100644
--- a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/deb/Messages.properties
+++ b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/deb/Messages.properties
@@ -22,6 +22,9 @@
option.wmclass.description=StartupWMClass to set in .desktop file. Should match the WMClass of the main application window.
option.category.description=Application category (or categories) to use in the .desktop file.
option.desktopfilename.description=Optional name for .desktop file (without suffix). Defaults to sanitized package name.
+option.control_template.description=Optional path to custom Debian Control file template.
+option.desktop_template.description=Optional path to custom .desktop file template.
+option.launcher_template.description=Optional path to custom launcher script template.
message.missingdebtools=The dpkg, dpkg-deb and fakeroot commands must be installed.
message.svgnoicon=The package.deb.icon-svg option is set, but package.deb.icon is not. Using default icons.
diff --git a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/deb/deb.desktop.template b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/deb/deb.desktop.template
index a8be994..90155f0 100644
--- a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/deb/deb.desktop.template
+++ b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/deb/deb.desktop.template
@@ -1,6 +1,6 @@
[Desktop Entry]
Encoding=UTF-8
-Name=${package.name} ${package.version}
+Name=${package.name}
Exec=${EXEC}
Icon=${ICON}
Categories=${package.deb.category}
diff --git a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/innosetup/InnoSetup.iss.template b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/innosetup/InnoSetup.iss.template
index 6c83ab7..6673de8 100644
--- a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/innosetup/InnoSetup.iss.template
+++ b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/innosetup/InnoSetup.iss.template
@@ -1,35 +1,35 @@
-[Setup]
-AppId=${APP_ID}
-AppName=${APP_NAME}
-AppVersion=${APP_VERSION}
-
-; TO-DO ${APP_URLS}
-${APP_LICENSE}
-
-DefaultDirName="{autopf}\${APP_NAME_SAFE}"
-DisableProgramGroupPage=yes
-OutputBaseFilename="${APP_NAME_SAFE} ${APP_VERSION}"
-SetupIconFile="${EXEC_NAME}\etc\${EXEC_NAME}.ico"
-Compression=lzma
-SolidCompression=yes
-ArchitecturesAllowed=x64
-ArchitecturesInstallIn64BitMode=x64
-
-[Languages]
-Name: "english"; MessagesFile: "compiler:Default.isl"
-
-[Tasks]
-Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
-
-[InstallDelete]
-${INSTALL_DELETE}
-
-[Files]
-${FILES}
-
-[Icons]
-Name: "{commonprograms}\${APP_NAME_SAFE}"; Filename: "{app}\bin\${EXEC_NAME}64.exe"; ${PARAMETERS} IconFilename: "{app}\etc\${EXEC_NAME}.ico";
-Name: "{commondesktop}\${APP_NAME_SAFE}"; Filename: "{app}\bin\${EXEC_NAME}64.exe"; ${PARAMETERS} IconFilename: "{app}\etc\${EXEC_NAME}.ico"; Tasks: desktopicon
-
-[Run]
-Filename: "{app}\bin\${EXEC_NAME}64.exe"; ${PARAMETERS} Description: "{cm:LaunchProgram,${APP_NAME_SAFE}}"; Flags: nowait postinstall skipifsilent
+[Setup]
+AppId=${APP_ID}
+AppName=${APP_NAME}
+AppVersion=${APP_VERSION}
+
+; TO-DO ${APP_URLS}
+${APP_LICENSE}
+
+DefaultDirName="{autopf}\${APP_NAME_SAFE}"
+DisableProgramGroupPage=yes
+OutputBaseFilename="${APP_NAME_SAFE} ${APP_VERSION}"
+SetupIconFile="${EXEC_NAME}\etc\${EXEC_NAME}.ico"
+Compression=lzma
+SolidCompression=yes
+ArchitecturesAllowed=x64
+ArchitecturesInstallIn64BitMode=x64
+
+[Languages]
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+[Tasks]
+Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
+
+[InstallDelete]
+${INSTALL_DELETE}
+
+[Files]
+${FILES}
+
+[Icons]
+Name: "{commonprograms}\${APP_NAME_SAFE}"; Filename: "{app}\bin\${EXEC_NAME}64.exe"; ${PARAMETERS} IconFilename: "{app}\etc\${EXEC_NAME}.ico";
+Name: "{commondesktop}\${APP_NAME_SAFE}"; Filename: "{app}\bin\${EXEC_NAME}64.exe"; ${PARAMETERS} IconFilename: "{app}\etc\${EXEC_NAME}.ico"; Tasks: desktopicon
+
+[Run]
+Filename: "{app}\bin\${EXEC_NAME}64.exe"; ${PARAMETERS} Description: "{cm:LaunchProgram,${APP_NAME_SAFE}}"; Flags: nowait postinstall skipifsilent
diff --git a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/macos/Messages.properties b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/macos/Messages.properties
index b120e58..fec0030 100644
--- a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/macos/Messages.properties
+++ b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/macos/Messages.properties
@@ -17,4 +17,15 @@
option.bundle_id.description=Value for CFBundleIdentifier.
option.icon.description=Path to icon file (*.icns). Defaults to Apache NetBeans logo.
-option.info_template.description=Optional Info.plist template.
+option.info_template.description=Optional path to Info.plist template.
+option.launcher_template.description=Optional path to launcher (main.swift) template.
+option.entitlements_template.description=Optional path to codesign entitlements template.
+option.codesign_files.description=Search pattern for native binaries that need to be code signed.
+option.codesign_jars.description=Search pattern for JARs that bundle native binaries that need to be code signed.
+option.codesign_id.description=Code signing identity as passed to Codesign.
+option.pkgbuild_id.description=Installer signing identity as passed to Pkgbuild.
+
+message.validatingtools=Validating required tools - {0}
+message.missingtool=Cannot find required tool - {0}
+message.nocodesignid=No codesign ID has been configured. App bundle will be unsigned.
+message.nopkgbuildid=No pkgbuild ID has been configured. Installer will be unsigned.
diff --git a/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/macos/sandbox.plist.template b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/macos/sandbox.plist.template
new file mode 100644
index 0000000..665ed16
--- /dev/null
+++ b/nbpackage/src/main/resources/org/apache/netbeans/nbpackage/macos/sandbox.plist.template
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+
+ <key>com.apple.security.cs.allow-jit</key>
+ <true/>
+
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
+ <true/>
+
+ <key>com.apple.security.cs.disable-library-validation</key>
+ <true/>
+
+ <key>com.apple.security.cs.allow-dyld-environment-variables</key>
+ <true/>
+
+ <key>com.apple.security.cs.debugger</key>
+ <true/>
+
+ </dict>
+</plist>
diff --git a/nbpackage/src/test/java/org/apache/netbeans/nbpackage/FileUtilsTest.java b/nbpackage/src/test/java/org/apache/netbeans/nbpackage/FileUtilsTest.java
new file mode 100644
index 0000000..9d9a906
--- /dev/null
+++ b/nbpackage/src/test/java/org/apache/netbeans/nbpackage/FileUtilsTest.java
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.netbeans.nbpackage;
+
+import java.net.URI;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ *
+ */
+public class FileUtilsTest {
+
+ public FileUtilsTest() {
+ }
+
+ @BeforeAll
+ public static void setUpClass() {
+ }
+
+ @AfterAll
+ public static void tearDownClass() {
+ }
+
+ @BeforeEach
+ public void setUp() {
+ }
+
+ @AfterEach
+ public void tearDown() {
+ }
+
+ /**
+ * Test of copyFiles method, of class FileUtils.
+ */
+ @Test
+ public void testCopyFiles() throws Exception {
+ Path src = Files.createTempDirectory("nbp-copy-src-");
+ Path parent = src.resolve("foo").resolve("bar");
+ Path file = parent.resolve("baz");
+ Files.createDirectories(parent);
+ Files.createFile(file);
+ assertTrue(Files.isRegularFile(file));
+ Path dst = Files.createTempDirectory("nbp-copy-dst-");
+ FileUtils.copyFiles(src, dst);
+ assertTrue(Files.isRegularFile(file));
+ Path dstFile = dst.resolve(src.relativize(file));
+ assertTrue(Files.isRegularFile(dstFile));
+ FileUtils.deleteFiles(src);
+ FileUtils.deleteFiles(dst);
+ }
+
+ /**
+ * Test of moveFiles method, of class FileUtils.
+ */
+ @Test
+ public void testMoveFiles() throws Exception {
+ Path src = Files.createTempDirectory("nbp-move-src-");
+ Path parent = src.resolve("foo").resolve("bar");
+ Path file = parent.resolve("baz");
+ Files.createDirectories(parent);
+ Files.createFile(file);
+ assertTrue(Files.isRegularFile(file));
+ Path dst = Files.createTempDirectory("nbp-move-dst-");
+ FileUtils.moveFiles(src, dst);
+ assertFalse(Files.isRegularFile(file));
+ Path dstFile = dst.resolve(src.relativize(file));
+ assertTrue(Files.isRegularFile(dstFile));
+ Files.delete(src);
+ FileUtils.deleteFiles(dst);
+ }
+
+ /**
+ * Test of deleteFiles method, of class FileUtils.
+ */
+ @Test
+ public void testDeleteFiles() throws Exception {
+ Path tmpDir = Files.createTempDirectory("nbp-delete-");
+ Path parent = tmpDir.resolve("foo").resolve("bar");
+ Path file = parent.resolve("baz");
+ Files.createDirectories(parent);
+ Files.createFile(file);
+ assertTrue(Files.isRegularFile(file));
+ FileUtils.deleteFiles(tmpDir.resolve("foo"));
+ assertFalse(Files.exists(file));
+ assertFalse(Files.exists(parent));
+ Files.delete(tmpDir);
+ }
+
+ /**
+ * Test of find method, of class FileUtils.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testFind() throws Exception {
+ Path tmpDir = Files.createTempDirectory("nbp-find-");
+ try {
+ List<Path> found = FileUtils.find(tmpDir, "**");
+ assertTrue(found.isEmpty());
+ Path dir = Files.createDirectory(tmpDir.resolve("dir"));
+ Path file1 = Files.createFile(dir.resolve("file1.ext1"));
+ Path file2 = Files.createFile(tmpDir.resolve("file2.ext2"));
+ found = FileUtils.find(tmpDir, "*");
+ assertEquals(3, found.size());
+ found = FileUtils.find(tmpDir, "dir/*");
+ assertTrue(found.size() == 1 && found.contains(file1));
+ found = FileUtils.find(tmpDir, "*.{ext1,ext2}");
+ assertTrue(found.size() == 2 && found.contains(file1) && found.contains(file2));
+ found = FileUtils.find(tmpDir, "**.{ext1,ext2}");
+ assertTrue(found.size() == 2 && found.contains(file1) && found.contains(file2));
+ found = FileUtils.find(tmpDir, "**/*.{ext1,ext2}");
+ assertTrue(found.size() == 1 && found.contains(file1));
+ found = FileUtils.find(tmpDir, "*/file2.ext2");
+ assertTrue(found.isEmpty());
+ } finally {
+ FileUtils.deleteFiles(tmpDir);
+ }
+ }
+
+
+ /**
+ * Test of findDirs method, of class FileUtils.
+ */
+ @Test
+ public void testFindDirs() throws Exception {
+ Path tmpDir = Files.createTempDirectory("nbp-find-dirs-");
+ Path foo = tmpDir.resolve("foo");
+ Path fooBin = foo.resolve("bin");
+ Files.createDirectories(fooBin);
+ Files.createFile(fooBin.resolve("launcher"));
+ Path fooEtc = foo.resolve("etc");
+ Files.createDirectories(fooEtc);
+ Files.createFile(fooEtc.resolve("test.conf"));
+ Path bar = tmpDir.resolve("bar");
+ Path barBin = bar.resolve("bin");
+ Files.createDirectories(barBin);
+ Files.createFile(barBin.resolve("launcher"));
+
+ List<Path> findDirs = FileUtils.findDirs(tmpDir, 1, "bin/launcher");
+
+ assertEquals(2, findDirs.size());
+ assertTrue(findDirs.contains(foo));
+ assertTrue(findDirs.contains(bar));
+
+ findDirs = FileUtils.findDirs(tmpDir, 1, "bin/launcher", "etc/*.conf");
+
+ assertEquals(1, findDirs.size());
+ assertTrue(findDirs.contains(foo));
+ assertFalse(findDirs.contains(bar));
+
+ FileUtils.deleteFiles(tmpDir);
+
+ }
+
+ @Test
+ public void testProcessJarContents() throws Exception {
+ Path tmpDir = Files.createTempDirectory("nbp-process-jar-");
+ try {
+ Path root = Files.createDirectory(tmpDir.resolve("root"));
+ Path dir1 = Files.createDirectory(root.resolve("dir1"));
+ Path dir2 = Files.createDirectory(root.resolve("dir2"));
+ Path file1 = Files.writeString(dir1.resolve("file1"),
+ "File One",
+ StandardOpenOption.CREATE_NEW);
+ Path file2 = Files.writeString(dir2.resolve("file2"),
+ "File Two",
+ StandardOpenOption.CREATE_NEW);
+ Path jarFile = tmpDir.resolve("test.jar");
+ FileUtils.createZipArchive(root, jarFile);
+
+ int[] found = new int[]{0};
+ boolean processed;
+
+ processed = FileUtils.processJarContents(jarFile, "**/*", (file, path) -> {
+ found[0] = found[0] + 1;
+ Files.writeString(file, "GARBAGE");
+ return false;
+ });
+
+ assertFalse(processed);
+ assertEquals(2, found[0]);
+
+ var jarURI = URI.create("jar:" + jarFile.toUri());
+
+ try (var jarFS = FileSystems.newFileSystem(jarURI, Map.of())) {
+ assertEquals("File One",
+ Files.readString(jarFS.getPath("dir1", "file1")));
+ assertEquals("File Two",
+ Files.readString(jarFS.getPath("dir2", "file2")));
+ }
+
+ found[0] = 0;
+ processed = FileUtils.processJarContents(jarFile, "/dir1/*", (file, path) -> {
+ found[0] = found[0] + 1;
+ assertEquals("file1", file.getFileName().toString());
+ assertEquals("/dir1/file1", path);
+ Files.writeString(file, "FILE ONE UPDATED");
+ return true;
+ });
+
+ assertTrue(processed);
+ assertEquals(1, found[0]);
+
+ try (var jarFS = FileSystems.newFileSystem(jarURI, Map.of())) {
+ assertEquals("FILE ONE UPDATED",
+ Files.readString(jarFS.getPath("dir1", "file1")));
+ assertEquals("File Two",
+ Files.readString(jarFS.getPath("dir2", "file2")));
+ }
+ } finally {
+ FileUtils.deleteFiles(tmpDir);
+ }
+
+ }
+
+}
diff --git a/nbpackage/src/test/java/org/apache/netbeans/nbpackage/TemplateTest.java b/nbpackage/src/test/java/org/apache/netbeans/nbpackage/TemplateTest.java
new file mode 100644
index 0000000..1ea6f68
--- /dev/null
+++ b/nbpackage/src/test/java/org/apache/netbeans/nbpackage/TemplateTest.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.netbeans.nbpackage;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ *
+ */
+public class TemplateTest {
+
+ public TemplateTest() {
+ }
+
+ /**
+ * Test of load method, of class Template.
+ */
+ @Test
+ public void testLoad() throws Exception {
+ Path tmpDir = Files.createTempDirectory("nbp-templates-");
+ try {
+ Option<Path> option1 = Option.ofPath("option1", "");
+ Option<Path> option2 = Option.ofPath("option2", "");
+ Template template1 = Template.of(option1, "Template 1",
+ () -> TemplateTest.class.getResourceAsStream("template1.template"));
+ Template template2 = Template.of(option2, "Template 2",
+ () -> {
+ throw new AssertionError("Default source should not be called");
+ });
+ Path override2 = Files.writeString(tmpDir.resolve("template2"),
+ "TEMPLATE TWO OVERRIDE", StandardOpenOption.CREATE_NEW);
+
+ Configuration config = Configuration.builder()
+ .set(option2, override2.toAbsolutePath().toString())
+ .build();
+ ExecutionContext ctxt = new ExecutionContext(null, null, config, null, false);
+ String loaded1 = template1.load(ctxt);
+ String loaded2 = template2.load(ctxt);
+ assertEquals("TEMPLATE ONE", loaded1);
+ assertEquals("TEMPLATE TWO OVERRIDE", loaded2);
+ } finally {
+ FileUtils.deleteFiles(tmpDir);
+ }
+ }
+
+}
diff --git a/nbpackage/src/test/resources/org/apache/netbeans/nbpackage/template1.template b/nbpackage/src/test/resources/org/apache/netbeans/nbpackage/template1.template
new file mode 100644
index 0000000..265485e
--- /dev/null
+++ b/nbpackage/src/test/resources/org/apache/netbeans/nbpackage/template1.template
@@ -0,0 +1 @@
+TEMPLATE ONE
\ No newline at end of file