Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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();
}

}
255 changes: 255 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,255 @@
/*
* 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 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,
"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