8232621: L10n issues with msi installers
Reviewed-by: herrick, almatvee
This commit is contained in:
parent
c55e52e01f
commit
ee2e61d7e0
src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal
WinMsiBundler.java
resources
test/jdk/tools/jpackage
@ -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");
|
||||
}
|
||||
|
238
test/jdk/tools/jpackage/windows/WinL10nTest.java
Normal file
238
test/jdk/tools/jpackage/windows/WinL10nTest.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user