Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/main/java/org/apache/netbeans/nbpackage/NBPackage.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.apache.netbeans.nbpackage.deb.DebPackager;
import org.apache.netbeans.nbpackage.innosetup.InnoSetupPackager;
import org.apache.netbeans.nbpackage.macos.PkgPackager;
import org.apache.netbeans.nbpackage.nsis.NsisPackager;
import org.apache.netbeans.nbpackage.rpm.RpmPackager;
import org.apache.netbeans.nbpackage.zip.ZipPackager;

Expand Down Expand Up @@ -118,11 +119,11 @@ public final class NBPackage {
MESSAGES.getString("option.remove.help"));

// @TODO generate list from service loader if modularizing
private static final List<Packager> PACKAGERS = List.of(
new AppImagePackager(),
private static final List<Packager> PACKAGERS = List.of(new AppImagePackager(),
new DebPackager(),
new RpmPackager(),
new InnoSetupPackager(),
new NsisPackager(),
new PkgPackager(),
new ZipPackager()
);
Expand Down
101 changes: 101 additions & 0 deletions src/main/java/org/apache/netbeans/nbpackage/nsis/NsisPackager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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.nsis;

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 Windows .exe installer using Nsis.
*/
public class NsisPackager implements Packager {

static final ResourceBundle MESSAGES
= ResourceBundle.getBundle(NsisPackager.class.getPackageName() + ".Messages");

/**
* InnoSetup App ID.
*/
static final Option<String> APPID
= Option.ofString("package.innosetup.appid", "",
MESSAGES.getString("option.appid.help"));

/**
* Path to icon file (*.ico).
*/
static final Option<Path> ICON_PATH
= Option.ofPath("package.innosetup.icon", "",
MESSAGES.getString("option.icon.help"));

/**
* Path to optional license file (*.txt or *.rtf) to display during
* installation.
*/
static final Option<Path> LICENSE_PATH
= Option.ofPath("package.innosetup.license", "",
MESSAGES.getString("option.license.help"));

/**
* Path to alternative InnoSetup template.
*/
static final Option<Path> NSH_TEMPLATE_PATH
= Option.ofPath("package.innosetup.template", "",
MESSAGES.getString("option.template.help"));

/**
* ISS file template.
*/
static final Template NSH_TEMPLATE
= Template.of(NSH_TEMPLATE_PATH, "windows.nsh.template",
() -> NsisPackager.class.getResourceAsStream("windows.nsh.template"));

private static final List<Option<?>> NSIS_OPTIONS
= List.of(APPID, ICON_PATH,
LICENSE_PATH, NSH_TEMPLATE_PATH);

private static final List<Template> NSIS_TEMPLATES
= List.of(NSH_TEMPLATE);

@Override
public Task createTask(ExecutionContext context) {
return new NsisTask(context);
}

@Override
public String name() {
return "windows-nsis";
}

@Override
public Stream<Option<?>> options() {
return NSIS_OPTIONS.stream();
}

@Override
public Stream<Template> templates() {
return NSIS_TEMPLATES.stream();
}

}
256 changes: 256 additions & 0 deletions src/main/java/org/apache/netbeans/nbpackage/nsis/NsisTask.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
/*
* 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.nsis;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.text.MessageFormat;
import java.util.Arrays;
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.FileUtils;
import org.apache.netbeans.nbpackage.NBPackage;
import org.apache.netbeans.nbpackage.StringUtils;

import static org.apache.netbeans.nbpackage.nsis.NsisPackager.*;

class NsisTask extends AbstractPackagerTask {

private static final String NSIS = "makensis";

NsisTask(ExecutionContext context) {
super(context);
}

@Override
protected void checkPackageRequirements() throws Exception {
if (context().isImageOnly()) {
validateTools(NSIS);
}
}

@Override
protected void customizeImage(Path image) throws Exception {
String execName = findExecName(image.resolve("APPDIR").resolve("bin"));

Path appDir = image.resolve(execName);
Files.move(image.resolve("APPDIR"), appDir);

setupIcons(image, execName);
setupLicenseFile(image);
}

@Override
protected void finalizeImage(Path image) throws Exception {
String execName = findExecName(FileUtils.find(image, "*/bin").get(0));
createInnoSetupScript(image, execName);
}

@Override
protected Path buildPackage(Path image) throws Exception {
Path nshFile;
try (var stream = Files.newDirectoryStream(image, "netbeans.nsh")) {
var itr = stream.iterator();
if (!itr.hasNext()) {
throw new IllegalArgumentException(image.toString());
}
nshFile = itr.next();
}
int result = context().exec(NSIS, nshFile.toAbsolutePath().toString());
if (result != 0) {
throw new Exception();
}

Path exeFile;
try (var stream = Files.newDirectoryStream(image, "*.exe")) {
var itr = stream.iterator();
if (!itr.hasNext()) {
throw new IllegalArgumentException(image.toString());
}
exeFile = itr.next();
}
Path output = context().destination().resolve(exeFile.getFileName());
Files.move(exeFile, output);
return output;
}

@Override
protected String calculateImageName(Path input) throws Exception {
return super.calculateImageName(input) + "-NSIS";
}

@Override
protected Path calculateAppPath(Path image) throws Exception {
return image.resolve("APPDIR");
}

@Override
protected Path calculateRuntimePath(Path image, Path application) throws Exception {
return application.resolve("jdk");
}

@Override
protected Path calculateRootPath(Path image) throws Exception {
return FileUtils.find(image, "*/bin").get(0).getParent();
}

private Path findLauncher(Path binDir) throws IOException {
try (var files = Files.list(binDir)) {
return files.filter(f -> f.getFileName().toString().endsWith("64.exe"))
.findFirst().orElseThrow(IOException::new);
}
}

private String findExecName(Path binDir) throws IOException {
var bin = findLauncher(binDir);
var name = bin.getFileName().toString();
return name.substring(0, name.length() - "64.exe".length());
}

private void setupIcons(Path image, String execName) throws IOException {
Path icoFile = context().getValue(ICON_PATH).orElse(null);
Path dstFile = image.resolve(execName)
.resolve("etc")
.resolve(execName + ".ico");
if (icoFile != null) {
Files.copy(icoFile, dstFile);
} else {
Files.copy(getClass().getResourceAsStream(
"/org/apache/netbeans/nbpackage/apache-netbeans.ico"),
dstFile
);
}
}

private void setupLicenseFile(Path image) throws IOException {
var license = context().getValue(LICENSE_PATH).orElse(null);
if (license == null) {
return;
}
var name = license.getFileName().toString().toLowerCase(Locale.ROOT);
var isTXT = name.endsWith(".txt");
var isRTF = name.endsWith(".rtf");
if (!isTXT && !isRTF) {
throw new IllegalArgumentException(license.toString());
}
var target = image.resolve(isTXT ? "license.txt" : "license.rtf");
Files.copy(license, target);
}

private void createInnoSetupScript(Path image, String execName) throws IOException {
// make sure loaded template has correct line endings
String template = NSH_TEMPLATE.load(context()).lines()
.collect(Collectors.joining("\r\n", "", "\r\n"));

List<Path> files;
try (var l = Files.list(image.resolve(execName))) {
files = l.sorted().collect(Collectors.toList());
}

String installDeleteSection = buildInstallDeleteSection(files);
String filesSection = buildFilesSection(execName, files);

String appName = context().getValue(NBPackage.PACKAGE_NAME).orElse(execName);
String appNameSafe = sanitize(appName);
String appID = context().getValue(APPID).orElse(appName);
String appVersion = context().getValue(NBPackage.PACKAGE_VERSION).orElse("1.0");
String publisher = context().getValue(NBPackage.PACKAGE_PUBLISHER).orElse("");
String appLicense;
if (Files.exists(image.resolve("license.txt"))) {
appLicense = "LicenseFile=license.txt";
} else if (Files.exists(image.resolve("license.rtf"))) {
appLicense = "LicenseFile=license.rtf";
} else {
appLicense = "";
}

String execParam = context().getValue(NBPackage.PACKAGE_RUNTIME)
.map(p -> "Parameters: \"--jdkhome \"\"{app}\\jdk\"\"\";")
.orElse("");

var map = Map.of("APP_ID", appID,
"APP_NAME", appName,
"APP_NAME_SAFE", appNameSafe,
"APP_VERSION", appVersion,
"APP_LICENSE", appLicense,
"PUBLISHER", publisher,
"INSTALL_DELETE", installDeleteSection,
"FILES", filesSection,
"EXEC_NAME", execName,
"PARAMETERS", execParam
);

String script = StringUtils.replaceTokens(template, map);
Files.writeString(image.resolve(execName + ".nsh"), script,
StandardOpenOption.CREATE_NEW);
// copy tooling
Files.write(image.resolve("AdvUninstLog.nsh"), NsisPackager.class.getResourceAsStream("AdvUninstLog.nsh").readAllBytes(),
StandardOpenOption.CREATE_NEW);
}

private String sanitize(String name) {
return name.replaceAll("[\\\\/:*?\"<>|]", "_");
}

private String buildInstallDeleteSection(List<Path> files) {
return files.stream()
.map(Path::getFileName)
.map(Path::toString)
.map(name -> "Type: filesandordirs; Name: \"{app}\\" + name + "\"")
.collect(Collectors.joining("\r\n", "", "\r\n"));
}

private String buildFilesSection(String execName, List<Path> files) {
return files.stream()
.map(file -> buildFilesSectionLine(execName, file))
.collect(Collectors.joining("\r\n", "", "\r\n"));
}

private String buildFilesSectionLine(String execName, Path file) {
boolean isDir = Files.isDirectory(file);
String fileName = file.getFileName().toString();
if (isDir) {
return "Source: \"" + execName + "\\" + fileName + "\\*\"; DestDir: \"{app}\\"
+ fileName + "\"; Flags: ignoreversion recursesubdirs createallsubdirs";
} else {
return "Source: \"" + execName + "\\" + fileName + "\"; DestDir: \"{app}\"; Flags: ignoreversion";
}
}

private void validateTools(String... tools) throws Exception {
if (context().isVerbose()) {
context().infoHandler().accept(MessageFormat.format(
MESSAGES.getString("message.validatingtools"),
Arrays.toString(tools)));
}
for (String tool : tools) {
if (context().exec(List.of("which", tool)) != 0) {
throw new IllegalStateException(
MESSAGES.getString("message.missingrpmtools"));
}
}
}

}
Loading