package jdk.incubator.jpackage.internal;
import java.io.*;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static jdk.incubator.jpackage.internal.DesktopIntegration.*;
import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES;
import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
abstract class LinuxPackageBundler extends AbstractBundler {
LinuxPackageBundler(BundlerParamInfo<String> packageName) {
this.packageName = packageName;
}
@Override
final public boolean validate(Map<String, ? super Object> params)
throws ConfigException {
APP_BUNDLER.fetchFrom(params).validate(params);
validateInstallDir(LINUX_INSTALL_DIR.fetchFrom(params));
validateFileAssociations(FILE_ASSOCIATIONS.fetchFrom(params));
packageName.getStringConverter().apply(packageName.fetchFrom(params),
params);
for (var validator: getToolValidators(params)) {
ConfigException ex = validator.validate();
if (ex != null) {
throw ex;
}
}
withFindNeededPackages = LibProvidersLookup.supported();
if (!withFindNeededPackages) {
final String advice;
if ("deb".equals(getID())) {
advice = "message.deb-ldd-not-available.advice";
} else {
advice = "message.rpm-ldd-not-available.advice";
}
Log.error(String.format("%s\n%s", I18N.getString(
"message.ldd-not-available"), I18N.getString(advice)));
}
doValidate(params);
return true;
}
@Override
final public String getBundleType() {
return "INSTALLER";
}
@Override
final public File execute(Map<String, ? super Object> params,
File outputParentDir) throws PackagerException {
IOUtils.writableOutputDir(outputParentDir.toPath());
PlatformPackage thePackage = createMetaPackage(params);
Function<File, ApplicationLayout> initAppImageLayout = imageRoot -> {
ApplicationLayout layout = appImageLayout(params);
layout.pathGroup().setPath(new Object(),
AppImageFile.getPathInAppImage(Path.of("")));
return layout.resolveAt(imageRoot.toPath());
};
try {
File appImage = StandardBundlerParam.getPredefinedAppImage(params);
if (appImage != null) {
initAppImageLayout.apply(appImage).copy(
thePackage.sourceApplicationLayout());
} else {
appImage = APP_BUNDLER.fetchFrom(params).doBundle(params,
thePackage.sourceRoot().toFile(), true);
ApplicationLayout srcAppLayout = initAppImageLayout.apply(
appImage);
if (appImage.equals(PREDEFINED_RUNTIME_IMAGE.fetchFrom(params))) {
srcAppLayout.copy(thePackage.sourceApplicationLayout());
} else {
srcAppLayout.move(thePackage.sourceApplicationLayout());
if (appImage.exists()) {
appImage.delete();
}
}
}
if (!StandardBundlerParam.isRuntimeInstaller(params)) {
desktopIntegration = new DesktopIntegration(thePackage, params);
} else {
desktopIntegration = null;
}
Map<String, String> data = createDefaultReplacementData(params);
if (desktopIntegration != null) {
data.putAll(desktopIntegration.create());
} else {
Stream.of(DESKTOP_COMMANDS_INSTALL, DESKTOP_COMMANDS_UNINSTALL,
UTILITY_SCRIPTS).forEach(v -> data.put(v, ""));
}
data.putAll(createReplacementData(params));
File packageBundle = buildPackageBundle(Collections.unmodifiableMap(
data), params, outputParentDir);
verifyOutputBundle(params, packageBundle.toPath()).stream()
.filter(Objects::nonNull)
.forEachOrdered(ex -> {
Log.verbose(ex.getLocalizedMessage());
Log.verbose(ex.getAdvice());
});
return packageBundle;
} catch (IOException ex) {
Log.verbose(ex);
throw new PackagerException(ex);
}
}
private List<String> getListOfNeededPackages(
Map<String, ? super Object> params) throws IOException {
PlatformPackage thePackage = createMetaPackage(params);
final List<String> xdgUtilsPackage;
if (desktopIntegration != null) {
xdgUtilsPackage = desktopIntegration.requiredPackages();
} else {
xdgUtilsPackage = Collections.emptyList();
}
final List<String> neededLibPackages;
if (withFindNeededPackages) {
LibProvidersLookup lookup = new LibProvidersLookup();
initLibProvidersLookup(params, lookup);
neededLibPackages = lookup.execute(thePackage.sourceRoot());
} else {
neededLibPackages = Collections.emptyList();
}
List<String> result = Stream.of(xdgUtilsPackage, neededLibPackages).flatMap(
List::stream).filter(Predicate.not(String::isEmpty)).sorted().distinct().collect(
Collectors.toList());
Log.verbose(String.format("Required packages: %s", result));
return result;
}
private Map<String, String> createDefaultReplacementData(
Map<String, ? super Object> params) throws IOException {
Map<String, String> data = new HashMap<>();
data.put("APPLICATION_PACKAGE", createMetaPackage(params).name());
data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
data.put("APPLICATION_VERSION", VERSION.fetchFrom(params));
data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params));
String defaultDeps = String.join(", ", getListOfNeededPackages(params));
String customDeps = LINUX_PACKAGE_DEPENDENCIES.fetchFrom(params).strip();
if (!customDeps.isEmpty() && !defaultDeps.isEmpty()) {
customDeps = ", " + customDeps;
}
data.put("PACKAGE_DEFAULT_DEPENDENCIES", defaultDeps);
data.put("PACKAGE_CUSTOM_DEPENDENCIES", customDeps);
return data;
}
abstract protected List<ConfigException> verifyOutputBundle(
Map<String, ? super Object> params, Path packageBundle);
abstract protected void initLibProvidersLookup(
Map<String, ? super Object> params,
LibProvidersLookup libProvidersLookup);
abstract protected List<ToolValidator> getToolValidators(
Map<String, ? super Object> params);
abstract protected void doValidate(Map<String, ? super Object> params)
throws ConfigException;
abstract protected Map<String, String> createReplacementData(
Map<String, ? super Object> params) throws IOException;
abstract protected File buildPackageBundle(
Map<String, String> replacementData,
Map<String, ? super Object> params, File outputParentDir) throws
PackagerException, IOException;
final protected PlatformPackage createMetaPackage(
Map<String, ? super Object> params) {
return new PlatformPackage() {
@Override
public String name() {
return packageName.fetchFrom(params);
}
@Override
public Path sourceRoot() {
return IMAGES_ROOT.fetchFrom(params).toPath().toAbsolutePath();
}
@Override
public ApplicationLayout sourceApplicationLayout() {
return appImageLayout(params).resolveAt(
applicationInstallDir(sourceRoot()));
}
@Override
public ApplicationLayout installedApplicationLayout() {
return appImageLayout(params).resolveAt(
applicationInstallDir(Path.of("/")));
}
private Path applicationInstallDir(Path root) {
Path installDir = Path.of(LINUX_INSTALL_DIR.fetchFrom(params),
name());
if (installDir.isAbsolute()) {
installDir = Path.of("." + installDir.toString()).normalize();
}
return root.resolve(installDir);
}
};
}
private ApplicationLayout appImageLayout(
Map<String, ? super Object> params) {
if (StandardBundlerParam.isRuntimeInstaller(params)) {
return ApplicationLayout.javaRuntime();
}
return ApplicationLayout.linuxAppImage();
}
private static void validateInstallDir(String installDir) throws
ConfigException {
if (installDir.startsWith("/usr/") || installDir.equals("/usr")) {
throw new ConfigException(MessageFormat.format(I18N.getString(
"error.unsupported-install-dir"), installDir), null);
}
if (installDir.isEmpty()) {
throw new ConfigException(MessageFormat.format(I18N.getString(
"error.invalid-install-dir"), "/"), null);
}
boolean valid = false;
try {
final Path installDirPath = Path.of(installDir);
valid = installDirPath.isAbsolute();
if (valid && !installDirPath.normalize().toString().equals(
installDirPath.toString())) {
valid = false;
}
} catch (InvalidPathException ex) {
}
if (!valid) {
throw new ConfigException(MessageFormat.format(I18N.getString(
"error.invalid-install-dir"), installDir), null);
}
}
private static void validateFileAssociations(
List<Map<String, ? super Object>> associations) throws
ConfigException {
int assocIdx = 0;
for (var assoc : associations) {
++assocIdx;
List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
if (mimes == null || mimes.isEmpty()) {
String msgKey = "error.no-content-types-for-file-association";
throw new ConfigException(
MessageFormat.format(I18N.getString(msgKey), assocIdx),
I18N.getString(msgKey + ".advise"));
}
if (mimes.size() > 1) {
String msgKey = "error.too-many-content-types-for-file-association";
throw new ConfigException(
MessageFormat.format(I18N.getString(msgKey), assocIdx),
I18N.getString(msgKey + ".advise"));
}
}
}
private final BundlerParamInfo<String> packageName;
private boolean withFindNeededPackages;
private DesktopIntegration desktopIntegration;
private static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER =
new StandardBundlerParam<>(
"linux.app.bundler",
LinuxAppBundler.class,
(params) -> new LinuxAppBundler(),
null
);
}