8232621: L10n issues with msi installers

Reviewed-by: herrick, almatvee
This commit is contained in:
Alexey Semenyuk 2020-08-12 11:38:30 -04:00
parent c55e52e01f
commit ee2e61d7e0
6 changed files with 330 additions and 10 deletions
src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal
test/jdk/tools/jpackage
helpers/jdk/jpackage/test
windows

@ -30,26 +30,42 @@ import java.io.InputStream;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.PathMatcher;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME;
import static jdk.incubator.jpackage.internal.StandardBundlerParam.CONFIG_ROOT;
import static jdk.incubator.jpackage.internal.StandardBundlerParam.DESCRIPTION;
import static jdk.incubator.jpackage.internal.StandardBundlerParam.LICENSE_FILE;
import static jdk.incubator.jpackage.internal.StandardBundlerParam.RESOURCE_DIR;
import static jdk.incubator.jpackage.internal.StandardBundlerParam.TEMP_ROOT;
import static jdk.incubator.jpackage.internal.StandardBundlerParam.VENDOR;
import static jdk.incubator.jpackage.internal.StandardBundlerParam.VERSION;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* WinMsiBundler
@ -416,7 +432,7 @@ public class WinMsiBundler extends AbstractBundler {
}
}
// Copy l10n files.
// Copy standard l10n files.
for (String loc : Arrays.asList("en", "ja", "zh_CN")) {
String fname = "MsiInstallerStrings_" + loc + ".wxl";
try (InputStream is = OverridableResource.readDefault(fname)) {
@ -470,9 +486,23 @@ public class WinMsiBundler extends AbstractBundler {
wixPipeline.addLightOptions("-ext", "WixUIExtension");
}
wixPipeline.addLightOptions("-loc",
CONFIG_ROOT.fetchFrom(params).resolve(I18N.getString(
"resource.wxl-file-name")).toAbsolutePath().toString());
final Path primaryWxlFile = CONFIG_ROOT.fetchFrom(params).resolve(
I18N.getString("resource.wxl-file-name")).toAbsolutePath();
wixPipeline.addLightOptions("-loc", primaryWxlFile.toString());
List<String> cultures = new ArrayList<>();
for (var wxl : getCustomWxlFiles(params)) {
wixPipeline.addLightOptions("-loc", wxl.toAbsolutePath().toString());
cultures.add(getCultureFromWxlFile(wxl));
}
cultures.add(getCultureFromWxlFile(primaryWxlFile));
// Build ordered list of unique cultures.
Set<String> uniqueCultures = new LinkedHashSet<>();
uniqueCultures.addAll(cultures);
wixPipeline.addLightOptions(uniqueCultures.stream().collect(
Collectors.joining(";", "-cultures:", "")));
// Only needed if we using CA dll, so Wix can find it
if (enableInstalldirUI) {
@ -485,6 +515,52 @@ public class WinMsiBundler extends AbstractBundler {
return msiOut;
}
private static List<Path> getCustomWxlFiles(Map<String, ? super Object> params)
throws IOException {
Path resourceDir = RESOURCE_DIR.fetchFrom(params);
if (resourceDir == null) {
return Collections.emptyList();
}
final String glob = "glob:**/*.wxl";
final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher(
glob);
try (var walk = Files.walk(resourceDir, 1)) {
return walk
.filter(Files::isReadable)
.filter(pathMatcher::matches)
.sorted((a, b) -> a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString()))
.collect(Collectors.toList());
}
}
private static String getCultureFromWxlFile(Path wxlPath) throws IOException {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(wxlPath.toFile());
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xPath.evaluate(
"//WixLocalization/@Culture", doc,
XPathConstants.NODESET);
if (nodes.getLength() != 1) {
throw new IOException(MessageFormat.format(I18N.getString(
"error.extract-culture-from-wix-l10n-file"),
wxlPath.toAbsolutePath()));
}
return nodes.item(0).getNodeValue();
} catch (XPathExpressionException | ParserConfigurationException
| SAXException ex) {
throw new IOException(MessageFormat.format(I18N.getString(
"error.read-wix-l10n-file"), wxlPath.toAbsolutePath()), ex);
}
}
private static void ensureByMutationFileIsRTF(Path f) {
if (f == null || !Files.isRegularFile(f)) return;

@ -48,6 +48,8 @@ error.msi-product-version-minor-out-of-range=Minor version must be in the range
error.version-swap=Failed to update version information for {0}
error.invalid-envvar=Invalid value of {0} environment variable
error.lock-resource=Failed to lock: {0}
error.read-wix-l10n-file=Failed to parse {0} file
error.extract-culture-from-wix-l10n-file=Failed to read value of culture from {0} file
message.icon-not-ico=The specified icon "{0}" is not an ICO file and will not be used. The default icon will be used in it's place.
message.potential.windows.defender.issue=Warning: Windows Defender may prevent jpackage from functioning. If there is an issue, it can be addressed by either disabling realtime monitoring, or adding an exclusion for the directory "{0}".

@ -48,6 +48,8 @@ error.msi-product-version-minor-out-of-range=\u30DE\u30A4\u30CA\u30FC\u30FB\u30D
error.version-swap={0}\u306E\u30D0\u30FC\u30B8\u30E7\u30F3\u60C5\u5831\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F
error.invalid-envvar={0}\u74B0\u5883\u5909\u6570\u306E\u5024\u304C\u7121\u52B9\u3067\u3059
error.lock-resource=\u30ED\u30C3\u30AF\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {0}
error.read-wix-l10n-file=Failed to parse {0} file
error.extract-culture-from-wix-l10n-file=Failed to read value of culture from {0} file
message.icon-not-ico=\u6307\u5B9A\u3057\u305F\u30A2\u30A4\u30B3\u30F3"{0}"\u306FICO\u30D5\u30A1\u30A4\u30EB\u3067\u306F\u306A\u304F\u3001\u4F7F\u7528\u3055\u308C\u307E\u305B\u3093\u3002\u30C7\u30D5\u30A9\u30EB\u30C8\u30FB\u30A2\u30A4\u30B3\u30F3\u304C\u305D\u306E\u4F4D\u7F6E\u306B\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002
message.potential.windows.defender.issue=\u8B66\u544A: Windows Defender\u304C\u539F\u56E0\u3067jpackage\u304C\u6A5F\u80FD\u3057\u306A\u3044\u3053\u3068\u304C\u3042\u308A\u307E\u3059\u3002\u554F\u984C\u304C\u767A\u751F\u3057\u305F\u5834\u5408\u306F\u3001\u30EA\u30A2\u30EB\u30BF\u30A4\u30E0\u30FB\u30E2\u30CB\u30BF\u30EA\u30F3\u30B0\u3092\u7121\u52B9\u306B\u3059\u308B\u304B\u3001\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA"{0}"\u306E\u9664\u5916\u3092\u8FFD\u52A0\u3059\u308B\u3053\u3068\u306B\u3088\u308A\u3001\u554F\u984C\u306B\u5BFE\u51E6\u3067\u304D\u307E\u3059\u3002

@ -48,6 +48,8 @@ error.msi-product-version-minor-out-of-range=\u6B21\u7248\u672C\u5FC5\u987B\u4F4
error.version-swap=\u65E0\u6CD5\u66F4\u65B0 {0} \u7684\u7248\u672C\u4FE1\u606F
error.invalid-envvar={0} \u73AF\u5883\u53D8\u91CF\u7684\u503C\u65E0\u6548
error.lock-resource=\u65E0\u6CD5\u9501\u5B9A\uFF1A{0}
error.read-wix-l10n-file=Failed to parse {0} file
error.extract-culture-from-wix-l10n-file=Failed to read value of culture from {0} file
message.icon-not-ico=\u6307\u5B9A\u7684\u56FE\u6807 "{0}" \u4E0D\u662F ICO \u6587\u4EF6, \u4E0D\u4F1A\u4F7F\u7528\u3002\u5C06\u4F7F\u7528\u9ED8\u8BA4\u56FE\u6807\u4EE3\u66FF\u3002
message.potential.windows.defender.issue=\u8B66\u544A\uFF1AWindows Defender \u53EF\u80FD\u4F1A\u963B\u6B62 jpackage \u6B63\u5E38\u5DE5\u4F5C\u3002\u5982\u679C\u5B58\u5728\u95EE\u9898\uFF0C\u53EF\u4EE5\u901A\u8FC7\u7981\u7528\u5B9E\u65F6\u76D1\u89C6\u6216\u8005\u4E3A\u76EE\u5F55 "{0}" \u6DFB\u52A0\u6392\u9664\u9879\u6765\u89E3\u51B3\u3002

@ -244,14 +244,14 @@ final public class TKit {
return v.subpath(0, v.getNameCount());
}
public static void createTextFile(Path propsFilename, Collection<String> lines) {
createTextFile(propsFilename, lines.stream());
public static void createTextFile(Path filename, Collection<String> lines) {
createTextFile(filename, lines.stream());
}
public static void createTextFile(Path propsFilename, Stream<String> lines) {
public static void createTextFile(Path filename, Stream<String> lines) {
trace(String.format("Create [%s] text file...",
propsFilename.toAbsolutePath().normalize()));
ThrowingRunnable.toRunnable(() -> Files.write(propsFilename,
filename.toAbsolutePath().normalize()));
ThrowingRunnable.toRunnable(() -> Files.write(filename,
lines.peek(TKit::trace).collect(Collectors.toList()))).run();
trace("Done");
}

@ -0,0 +1,238 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.IOException;
import java.nio.file.Path;
import jdk.jpackage.test.TKit;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.Annotations.Parameters;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.Collectors;
import jdk.jpackage.test.Executor;
/*
* @test
* @summary Custom l10n of msi installers in jpackage
* @library ../helpers
* @key jpackagePlatformPackage
* @requires (jpackage.test.SQETest == null)
* @build jdk.jpackage.test.*
* @requires (os.family == "windows")
* @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal
* @compile WinL10nTest.java
* @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=WinL10nTest
*/
public class WinL10nTest {
public WinL10nTest(WixFileInitializer wxlFileInitializers[],
String expectedCulture, String expectedErrorMessage) {
this.wxlFileInitializers = wxlFileInitializers;
this.expectedCulture = expectedCulture;
this.expectedErrorMessage = expectedErrorMessage;
}
@Parameters
public static List<Object[]> data() {
return List.of(new Object[][]{
{null, "en-us", null},
{new WixFileInitializer[] {
WixFileInitializer.create("a.wxl", "en-us")
}, "en-us", null},
{new WixFileInitializer[] {
WixFileInitializer.create("a.wxl", "fr")
}, "fr;en-us", null},
{new WixFileInitializer[] {
WixFileInitializer.create("a.wxl", "fr"),
WixFileInitializer.create("b.wxl", "fr")
}, "fr;en-us", null},
{new WixFileInitializer[] {
WixFileInitializer.create("a.wxl", "it"),
WixFileInitializer.create("b.wxl", "fr")
}, "it;fr;en-us", null},
{new WixFileInitializer[] {
WixFileInitializer.create("c.wxl", "it"),
WixFileInitializer.create("b.wxl", "fr")
}, "fr;it;en-us", null},
{new WixFileInitializer[] {
WixFileInitializer.create("a.wxl", "fr"),
WixFileInitializer.create("b.wxl", "it"),
WixFileInitializer.create("c.wxl", "fr"),
WixFileInitializer.create("d.wxl", "it")
}, "fr;it;en-us", null},
{new WixFileInitializer[] {
WixFileInitializer.create("c.wxl", "it"),
WixFileInitializer.createMalformed("b.wxl")
}, null, null}
});
}
private final static Stream<String> getLightCommandLine(
Executor.Result result) {
return result.getOutput().stream()
.filter(s -> s.contains("Running"))
.filter(s -> s.contains("light.exe"))
.filter(s -> !s.contains("/?"));
}
@Test
public void test() throws IOException {
final boolean allWxlFilesValid;
if (wxlFileInitializers != null) {
allWxlFilesValid = Stream.of(wxlFileInitializers).allMatch(
WixFileInitializer::isValid);
} else {
allWxlFilesValid = true;
}
PackageTest test = new PackageTest()
.forTypes(PackageType.WINDOWS)
.configureHelloApp()
.addInitializer(cmd -> {
// 1. Set fake run time to save time by skipping jlink step of jpackage.
// 2. Instruct test to save jpackage output.
cmd.setFakeRuntime().saveConsoleOutput(true);
})
.addBundleVerifier((cmd, result) -> {
if (expectedCulture != null) {
TKit.assertTextStream("-cultures:" + expectedCulture).apply(
getLightCommandLine(result));
}
if (expectedErrorMessage != null) {
TKit.assertTextStream(expectedErrorMessage)
.apply(result.getOutput().stream());
}
if (wxlFileInitializers != null) {
if (allWxlFilesValid) {
for (var v : wxlFileInitializers) {
v.createCmdOutputVerifier(resourceDir).apply(
getLightCommandLine(result));
}
} else {
Stream.of(wxlFileInitializers)
.filter(Predicate.not(WixFileInitializer::isValid))
.forEach(v -> v.createCmdOutputVerifier(
resourceDir).apply(result.getOutput().stream()));
TKit.assertFalse(
getLightCommandLine(result).findAny().isPresent(),
"Check light.exe was not invoked");
}
}
});
if (wxlFileInitializers != null) {
test.addInitializer(cmd -> {
resourceDir = TKit.createTempDirectory("resources");
cmd.addArguments("--resource-dir", resourceDir);
for (var v : wxlFileInitializers) {
v.apply(resourceDir);
}
});
}
if (expectedErrorMessage != null || !allWxlFilesValid) {
test.setExpectedExitCode(1);
}
test.run();
}
final private WixFileInitializer wxlFileInitializers[];
final private String expectedCulture;
final private String expectedErrorMessage;
private Path resourceDir;
private static class WixFileInitializer {
static WixFileInitializer create(String name, String culture) {
return new WixFileInitializer(name, culture);
}
static WixFileInitializer createMalformed(String name) {
return new WixFileInitializer(name, null) {
@Override
public void apply(Path root) throws IOException {
TKit.createTextFile(root.resolve(name), List.of(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>",
"<WixLocalization>"));
}
@Override
public String toString() {
return String.format("name=%s; malformed xml", name);
}
@Override
boolean isValid() {
return false;
}
@Override
TKit.TextStreamVerifier createCmdOutputVerifier(Path root) {
return TKit.assertTextStream(String.format(
"Failed to parse %s file",
root.resolve("b.wxl").toAbsolutePath()));
}
};
}
private WixFileInitializer(String name, String culture) {
this.name = name;
this.culture = culture;
}
void apply(Path root) throws IOException {
TKit.createTextFile(root.resolve(name), List.of(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>",
culture == null ? "<WixLocalization/>" : "<WixLocalization Culture=\""
+ culture
+ "\" xmlns=\"http://schemas.microsoft.com/wix/2006/localization\" Codepage=\"1252\"/>"));
}
TKit.TextStreamVerifier createCmdOutputVerifier(Path root) {
return TKit.assertTextStream(
root.resolve(name).toAbsolutePath().toString());
}
boolean isValid() {
return true;
}
@Override
public String toString() {
return String.format("name=%s; culture=%s", name, culture);
}
private final String name;
private final String culture;
}
}