8283707: Support <major.minor.update.build> version format on Windows

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2022-07-19 16:59:28 +00:00
parent 96a542feb2
commit 977e09489d
15 changed files with 1303 additions and 65 deletions

View File

@ -129,6 +129,10 @@ ifeq ($(call isTargetOs, windows), true)
JPACKAGE_TARGETS += $(BUILD_LIB_JPACKAGE)
JPACKAGE_WIXHELPER_SRC := \
$(call FindSrcDirsForComponent, jdk.jpackage, libwixhelper) \
$(call FindSrcDirsForComponent, jdk.jpackage, common)
# Build Wix custom action helper
# Output library in resources dir, and symbols in the object dir
$(eval $(call SetupJdkLibrary, BUILD_LIB_WIXHELPER, \
@ -136,11 +140,12 @@ ifeq ($(call isTargetOs, windows), true)
OUTPUT_DIR := $(JPACKAGE_OUTPUT_DIR), \
SYMBOLS_DIR := $(SUPPORT_OUTPUTDIR)/native/$(MODULE)/libwixhelper, \
OPTIMIZATION := LOW, \
SRC := $(JPACKAGE_WIXHELPER_SRC), \
CXXFLAGS := $(call JpackageWithStaticCrt, $(CXXFLAGS_JDKLIB)) \
$(JPACKAGE_CXXFLAGS_windows), \
$(addprefix -I, $(JPACKAGE_WIXHELPER_SRC)) $(JPACKAGE_CXXFLAGS_windows), \
LDFLAGS := $(LDFLAGS_JDKLIB) $(LDFLAGS_CXX_JDK), \
LIBS := $(LIBCXX), \
LIBS_windows := msi.lib Shlwapi.lib User32.lib, \
LIBS_windows := User32.lib, \
))
JPACKAGE_TARGETS += $(BUILD_LIB_WIXHELPER)

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 2022, 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
@ -36,7 +36,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
@ -105,7 +104,7 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
}
// Only needed if we using CA dll, so Wix can find it
if (withInstallDirChooserDlg) {
if (withCustomActionsDll) {
wixPipeline.addLightOptions("-b",
getConfigRoot().toAbsolutePath().toString());
}
@ -119,7 +118,7 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
void addFilesToConfigRoot() throws IOException {
super.addFilesToConfigRoot();
if (withInstallDirChooserDlg) {
if (withCustomActionsDll) {
String fname = "wixhelper.dll"; // CA dll
try (InputStream is = OverridableResource.readDefault(fname)) {
Files.copy(is, getConfigRoot().resolve(fname));
@ -481,6 +480,7 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
private boolean withInstallDirChooserDlg;
private boolean withShortcutPromptDlg;
private boolean withLicenseDlg;
private boolean withCustomActionsDll = true;
private List<CustomDialog> customDialogs;
private static final BundlerParamInfo<Boolean> INSTALLDIR_CHOOSER

View File

@ -31,7 +31,6 @@
<Fragment>
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
<CustomAction Id="JpCheckInstallDir" BinaryKey="JpCaDll" DllEntry="CheckInstallDir" />
<UI>

View File

@ -65,6 +65,10 @@
<CustomAction Id="JpDisallowDowngrade" Error="!(loc.DowngradeErrorMessage)" />
<?endif?>
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
<CustomAction Id="JpFindRelatedProducts" BinaryKey="JpCaDll" DllEntry="FindRelatedProductsEx" />
<!-- Standard required root -->
<Directory Id="TARGETDIR" Name="SourceDir"/>
@ -114,13 +118,18 @@
<?endif?>
<?ifndef JpAllowUpgrades ?>
<Custom Action="JpDisallowUpgrade" After="FindRelatedProducts">JP_UPGRADABLE_FOUND</Custom>
<Custom Action="JpDisallowUpgrade" After="JpFindRelatedProducts">JP_UPGRADABLE_FOUND</Custom>
<?endif?>
<?ifndef JpAllowDowngrades ?>
<Custom Action="JpDisallowDowngrade" After="FindRelatedProducts">JP_DOWNGRADABLE_FOUND</Custom>
<Custom Action="JpDisallowDowngrade" After="JpFindRelatedProducts">JP_DOWNGRADABLE_FOUND</Custom>
<?endif?>
<RemoveExistingProducts Before="CostInitialize"/>
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
</InstallExecuteSequence>
<InstallUISequence>
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
</InstallUISequence>
</Product>
</Wix>

View File

@ -0,0 +1,233 @@
/*
* Copyright (c) 2022, 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.
*/
#include "MsiCA.h"
#include "MsiDb.h"
#include "MsiUtils.h"
#include "FileUtils.h"
#include "ErrorHandling.h"
#include "Toolbox.h"
#pragma comment(lib, "msi.lib")
namespace msi {
tstring CAImpl::getProperty(const tstring& name) const {
return getPropertyFromCustomAction(handle, name);
}
void CAImpl::setProperty(const tstring& name, const tstring& value) {
if (value.empty()) {
JP_THROW(tstrings::any() << "Attempt to assign empty value to '"
<< name << "' MSI property");
}
LOG_TRACE(tstrings::any() << "Setting MSI property '" << name <<
"' to '" << value << "'");
const UINT status = MsiSetProperty(handle, name.c_str(), value.c_str());
if (status != ERROR_SUCCESS) {
JP_THROW(msi::Error(tstrings::any() << "MsiSetProperty(" << name
<< ", " << value << ") failed", status));
}
}
void CAImpl::removeProperty(const tstring& name) {
LOG_TRACE(tstrings::any() << "Removing MSI property '" << name << "'");
const UINT status = MsiSetProperty(handle, name.c_str(), NULL);
if (status != ERROR_SUCCESS) {
JP_THROW(msi::Error(tstrings::any() << "MsiSetProperty(" << name
<< ", NULL) failed", status));
}
}
Guid CAFacade::getProductCode() const {
return impl.getProperty(_T("ProductCode"));
}
bool CAFacade::isInMode(MSIRUNMODE v) const {
return MsiGetMode(impl.getHandle(), v) != FALSE;
}
tstring CAFacade::getModes() const {
tstring modes;
// Iterate all modes in the range [MSIRUNMODE_ADMIN, MSIRUNMODE_COMMIT]
for (int mode = MSIRUNMODE_ADMIN; mode != MSIRUNMODE_COMMIT + 1; ++mode) {
modes.insert(modes.end(), isInMode(MSIRUNMODE(mode)) ?
_T('1') : _T('0'));
}
return modes;
}
void CAFacade::doAction(const tstring& name) const {
const UINT status = MsiDoAction(impl.getHandle(), name.c_str());
if (status != ERROR_SUCCESS) {
JP_THROW(msi::Error(tstrings::any() << "MsiDoAction(" << name
<< ") failed", status));
}
}
tstring CAFacade::normalizeDirectoryPath(tstring v) {
if (v.empty()) {
return v;
}
std::replace(v.begin(), v.end(), '/', '\\');
return FileUtils::removeTrailingSlash(v) + _T("\\");
}
CA& CA::setPropertyIfEmpty(const tstring& name, const tstring& v) {
if (getProperty(name).empty()) {
setProperty(name, v);
}
return *this;
}
tstring DeferredCA::getArg() const {
if (isInMode(MSIRUNMODE_SCHEDULED) || caArgPropertyName.empty()) {
// Details on accessing MSI properties from deferred custom actions:
// http://blogs.technet.com/b/alexshev/archive/2008/03/25/property-does-not-exist-or-empty-when-accessed-from-deferred-custom-action.aspx
// http://stackoverflow.com/questions/17988392/unable-to-fetch-the-install-location-property-in-a-deferred-custom-action
// http://stackoverflow.com/questions/11233267/how-to-pass-customactiondata-to-a-customaction-using-wix
return impl.getProperty(_T("CustomActionData"));
}
return impl.getProperty(caArgPropertyName);
}
tstring DeferredCA::getParsedArg(const tstring& name) const {
const auto entry = theParsedArgs.find(name);
if (entry == theParsedArgs.end()) {
JP_THROW(tstrings::any() << "Argument << '" << name
<< "' not found.");
}
return entry->second;
}
namespace {
std::pair<tstring, tstring> parseArg(const tstring& v) {
const auto pos = v.find(_T('='));
if (pos == tstring::npos) {
JP_THROW(tstrings::any() << "Missing expected '=' character in ["
<< v << "] string.");
}
return std::pair<tstring, tstring>(v.substr(0, pos), v.substr(pos + 1));
}
void parseArgsImpl(DeferredCA::ArgsCtnr& dst, const tstring& src) {
const tstring_array pairs = tstrings::split(src, _T("*"));
for(auto it = pairs.begin(), end = pairs.end(); it != end; ++it) {
const auto pair = parseArg(*it);
dst[pair.first] = pair.second;
}
}
} // namespace
void DeferredCA::parseArgs(ArgsCtnr& dst, const tstring& src) {
DeferredCA::ArgsCtnr tmp;
const auto end = src.find(_T("**"));
if (end != tstring::npos) {
parseArgsImpl(tmp, src.substr(0, end));
tmp[tstring()] = src.substr(end + 2);
} else {
parseArgsImpl(tmp, src);
}
tmp.insert(dst.begin(), dst.end());
tmp.swap(dst);
}
MsiLogAppender::MsiLogAppender(MSIHANDLE h): handle(h),
ctorThread(GetCurrentThreadId()) {
}
void MsiLogAppender::append(const LogEvent& v) {
const LPCTSTR format = _T("[%02u:%02u:%02u.%03u%s%s:%u (%s)] %s: %s");
tstring ctxInfo = _T(" ");
if (v.tid != ctorThread) {
ctxInfo = (tstrings::any() << " (TID: " << v.tid << ") ").tstr();
}
const tstring buf = tstrings::unsafe_format(format,
unsigned(v.ts.wHour), unsigned(v.ts.wMinute), unsigned(v.ts.wSecond), unsigned(v.ts.wMilliseconds), // time
ctxInfo.c_str(),
v.fileName.c_str(), v.lineNum, v.funcName.c_str(),
v.logLevel.c_str(),
v.message.c_str());
DatabaseRecord r(1);
r.setString(0, _T("Java [1]"));
r.setString(1, buf);
MsiProcessMessage(handle, INSTALLMESSAGE_INFO, r.getHandle());
}
MsiLogTrigger::MsiLogTrigger(MSIHANDLE h):
msiLogAppender(h),
oldLogAppender(Logger::defaultLogger().getAppender()),
teeLogAppender(&msiLogAppender, &oldLogAppender) {
Logger::defaultLogger().setAppender(teeLogAppender);
}
MsiLogTrigger::~MsiLogTrigger() {
Logger::defaultLogger().setAppender(oldLogAppender);
}
namespace {
MSIHANDLE openDatabase(const CA& ca) {
MSIHANDLE h = MsiGetActiveDatabase(ca.getHandle());
if (h == NULL) {
JP_THROW(Error(std::string("MsiGetActiveDatabase() failed"),
ERROR_FUNCTION_FAILED));
}
return h;
}
} // namespace
Database::Database(const CA& ca): msiPath(_T("*CA*")),
dbHandle(openDatabase(ca)) {
}
} // namespace msi

View File

@ -0,0 +1,298 @@
/*
* Copyright (c) 2022, 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.
*/
#ifndef MsiCA_h
#define MsiCA_h
#include <windows.h>
#include <Msi.h>
#include <msidefs.h>
#include <msiquery.h>
#include <map>
#include "Log.h"
#include "Guid.h"
/**
* Helpers to implement custom actions (CA).
*/
namespace msi {
/**
* Return values from CA functions.
*/
struct CAStatus {
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa368072(v=vs.85).aspx
enum Values {
Success = ERROR_SUCCESS,
// Abort installation session.
UserExit = ERROR_INSTALL_USEREXIT,
// Unexpected error interrupted installation session.
FatalError = ERROR_INSTALL_FAILURE,
// Complete installation session without running further actions.
ExitNoError = ERROR_NO_MORE_ITEMS
};
};
/**
* Wrapper around MSIHANDLE passed in CA function by MSI service.
* Provides basic functionality to read/write property into the current MSI
* session.
*/
class CAImpl {
public:
explicit CAImpl(MSIHANDLE h): handle(h) {
}
/**
* Returns value of a property with the given name.
* Returns empty string if property with the given name doesn't exist.
* Throws exception if error occurs.
*/
tstring getProperty(const tstring& name) const;
/**
* Sets property value.
* Throws exception if error occurs.
* Throws exception if value is empty string.
*/
void setProperty(const tstring& name, const tstring& v);
/**
* Removes property.
* Throws exception if error occurs.
*/
void removeProperty(const tstring& name);
MSIHANDLE getHandle() const {
return handle;
}
private:
CAImpl(const CAImpl&);
CAImpl& operator=(const CAImpl&);
private:
MSIHANDLE handle;
};
/**
* Provides common functionality for deferred and immediate CAs.
*/
class CAFacade: public CAStatus {
public:
explicit CAFacade(MSIHANDLE h, UINT* status=NULL): impl(h), status(status) {
}
Guid getProductCode() const;
bool isInMode(MSIRUNMODE v) const;
// Debug
tstring getModes() const;
void exitStatus(CAStatus::Values v) {
if (status) {
*status = v;
}
}
void doAction(const tstring& name) const;
// Replaces all forward slashes with back slashes and ensures
// the last character is a backslash.
// Terminating directory paths with backslash is standard for MSI.
// Do nothing if 'path' is empty string.
static tstring normalizeDirectoryPath(tstring path);
protected:
CAImpl impl;
UINT* status;
};
/**
* Immediate CA.
*/
class CA: public CAFacade {
public:
CA(MSIHANDLE h, const tstring& /* name */,
UINT* status=NULL): CAFacade(h, status) {
}
tstring getProperty(const tstring& name) const {
return impl.getProperty(name);
}
CA& setProperty(const tstring& name, const tstring& v) {
impl.setProperty(name, v);
return *this;
}
CA& removeProperty(const tstring& name) {
impl.removeProperty(name);
return *this;
}
/**
* Like setProperty(), but do nothing if property with the given name
* exists and its value is not empty.
*/
CA& setPropertyIfEmpty(const tstring& name, const tstring& v);
MSIHANDLE getHandle() const {
return impl.getHandle();
}
};
/**
* Deferred CA.
*/
class DeferredCA: public CAFacade {
public:
DeferredCA(MSIHANDLE h, const tstring& name,
UINT* status=NULL): CAFacade(h, status), caArgPropertyName(name) {
}
typedef std::map<tstring, tstring> ArgsCtnr;
DeferredCA& parseArgs() {
parseArgs(theParsedArgs, getArg());
return *this;
}
tstring getArg() const;
const ArgsCtnr& parsedArgs() const {
return theParsedArgs;
}
tstring getParsedArg(const tstring& name) const;
static void parseArgs(ArgsCtnr& dst, const tstring& src);
private:
ArgsCtnr theParsedArgs;
tstring caArgPropertyName;
};
/**
* Write log messages into MSI log.
*/
class MsiLogAppender: public LogAppender {
public:
explicit MsiLogAppender(MSIHANDLE h);
virtual void append(const LogEvent& v);
private:
MSIHANDLE handle;
long ctorThread;
};
/**
* Configures logging for the current CA.
* Log messages that we send with LOG_INFO, LOG_ERROR, etc., go to both
* the existing log appender and temporary MSI log file managed by
* MSI service for the running MSI session (if any).
*/
class MsiLogTrigger {
public:
explicit MsiLogTrigger(MSIHANDLE h);
~MsiLogTrigger();
private:
MsiLogAppender msiLogAppender;
LogAppender& oldLogAppender;
TeeLogAppender teeLogAppender;
};
} // namespace msi
//
// Helpers to define CA functions.
//
// Sample usage:
// Define immediate CA foo:
// JP_CA(foo) {
// // `ca` is a local variable of type msi::CA.
// LOG_TRACE(ca.getProperty("Some property"));
// }
//
// Define deferred CA bar:
// JP_DEFERRED_CA(bar) {
// // `ca` is a local variable of type msi::DeferredCA.
// LOG_TRACE(ca.getArg());
// }
//
// JP_DEFERRED_CA/JP_CA macros take care of everything related to setup CA
// handler:
// - define CA function with the right calling convention and arguments
// expected by MSI;
// - construct local instance of either DeferredCA or CA type to access data
// in the running MSI session;
// - setup logging, so that log messages issues with LOG_INFO, LOG_ERROR, etc.
// macros go to MSI log file;
// - registers CA function with linker, so there is no need to manage
// separate .def file with the list of CA functions explicitly.
//
#define JP_CA_BASE(name, ca_type) \
static void name ## Body(ca_type&); \
extern "C" UINT __stdcall name(MSIHANDLE hInstall) { \
__pragma(comment(linker, "/EXPORT:" __FUNCTION__ "=" __FUNCDNAME__)); \
const msi::MsiLogTrigger logTrigger(hInstall); \
JP_DEBUG_BREAK(JP_CA_DEBUG_BREAK, name); \
LOG_TRACE_FUNCTION(); \
JP_TRY; \
UINT status = ca_type::Success; \
ca_type ca(hInstall, _T(#name), &status); \
LOG_TRACE(tstrings::any() << "CA modes=[" << ca.getModes() << "]"); \
name ## Body(ca); \
return status; \
JP_CATCH_ALL; \
return ca_type::FatalError; \
} \
static void name ## Body(ca_type& ca)
#define JP_CA(name) JP_CA_BASE(name, msi::CA)
#define JP_DEFERRED_CA(name) JP_CA_BASE(name, msi::DeferredCA)
#define JP_CA_DECLARE(name) \
extern "C" UINT __stdcall name(MSIHANDLE); \
__pragma(comment(linker, "/INCLUDE:" JP_CA_MANGLED_NAME(name)))
#ifdef _WIN64
#define JP_CA_MANGLED_NAME(name) #name
#else
#define JP_CA_MANGLED_NAME(name) "_" #name "@4"
#endif
#endif // #ifndef MsiCA_h

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2022, 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
@ -124,3 +124,25 @@ std::wstring SysError::getComErrorMessage(HRESULT hr) {
return tstrings::format(_T("COM error 0x%08X (%s)"), hrOrig,
getSystemMessageDescription(hr, NULL));
}
void debugBreak(const SourceCodePos& location, const tstring& envVarName,
const tstring& substr) {
if (!SysInfo::isEnvVariableSet(envVarName)) {
return;
}
const tstring v = SysInfo::getEnvVariable(std::nothrow, envVarName);
if (v != _T("*") && v.find(substr) == tstring::npos) {
return;
}
tstring msg = tstrings::fromUtf8(makeMessage(
std::runtime_error((tstrings::any()
<< "debug break. Reason: environment variable "
<< envVarName << " is set.").str()),
location));
tstring caption = FileUtils::basename(SysInfo::getCurrentModulePath());
MessageBoxW(NULL, msg.c_str(), caption.c_str(), MB_OK | MB_TASKMODAL);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2022, 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
@ -46,4 +46,18 @@ public:
static std::wstring getComErrorMessage(HRESULT hr);
};
/**
* Debug break. The function tests if the given environment variable is set.
* If it is and value of 'substr' parameter is a substring of value of
* the given environment variable message box is popped up waiting for
* user input.
* If value of environment variable is '*', value of 'substr' parameter is not
* tested and message box is popped up unconditionally.
*/
void debugBreak(const SourceCodePos& location, const tstring& envVarName,
const tstring& substr);
#define JP_DEBUG_BREAK(env, substr) ::debugBreak(JP_SOURCE_CODE_POS, _T(#env), _T(#substr))
#endif // #ifndef WinErrorHandling_h

View File

@ -0,0 +1,117 @@
/*
* Copyright (c) 2022, 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.
*/
#include <algorithm>
#define NOMINMAX
#include "Version.h"
namespace VersionDetails {
size_t Parser::operator ()(const tstring& str, int& buffer,
size_t& bufferSize) const {
if (bufferSize < 1) {
JP_THROW(tstrings::any() << "Destination buffer can't be empty");
}
tstring_array strComponents;
tstrings::split(strComponents, str, _T("."));
// Temporary storage. Needed to preserve destination buffer from
// partial update if parsing fails.
std::vector<int> recognizedComponents;
tstring_array::const_iterator it = strComponents.begin();
tstring_array::const_iterator end =
it + std::min(strComponents.size(), bufferSize);
// Number of successfully parsed characters in 'str'.
size_t cursor = 0;
while (it != end) {
const tstring& strComponent(*it);
try {
recognizedComponents.push_back(parseComponent(strComponent));
} catch (const std::exception&) {
// error parsing version component
break;
}
cursor += strComponent.size();
if (++it != end) {
++cursor;
}
}
if (str.size() < cursor) {
// Should never happen.
JP_THROW(tstrings::any()
<< "[" << cursor << " < " << str.size() << "] failed");
}
// Publish results only after successful parse.
bufferSize = recognizedComponents.size();
if (bufferSize) {
memcpy(&buffer, &*recognizedComponents.begin(),
bufferSize * sizeof(buffer));
}
if (!strComponents.empty() && strComponents.back().size() == 0
&& str.size() == cursor) {
// Input string ends with dot character (.). Mark it as unrecognized.
--cursor;
}
return (str.size() - cursor);
}
int parseComponent (const tstring& str) {
tistringstream input(str);
do {
if (str.empty() || !isdigit(str[0])) {
break;
}
int reply;
input >> reply;
if (!input.eof() || input.fail()) {
break;
}
return reply;
} while (false);
JP_THROW(tstrings::any()
<< "Failed to recognize version component in [" << str << "]");
}
} // namespace VersionDetails

View File

@ -0,0 +1,173 @@
/*
* Copyright (c) 2022, 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.
*/
#ifndef Version_h
#define Version_h
#include <cstring>
#include <stdexcept>
#include "tstrings.h"
#include "ErrorHandling.h"
/**
* Generic version. Version is given with string.
* String consists of version components separated with dot char (.).
* E.g.: 1.45.6.778.89
* Maximum number of components in version string is specified with 'N'
* template parameter.
*/
namespace VersionDetails {
struct Parser {
/**
* Parses version components from the given string.
* Returns number of trailing unrecognised characters or 0 if the whole
* string has been recognized.
*
* @param str
* string to parse;
* @param buffer
* reference to buffer accepting parsed version components;
* @param bufferSize[in,out]
* number of elements in the destination buffer; on return is set
* a number of components recognized in the input string.
*/
size_t operator () (const tstring& str, int& buffer,
size_t& bufferSize) const;
};
/**
* Returns parsed single version component from the given string.
* Throws std::exception if error occurs parsing the given string.
*
* @param str
* string to parse;
*/
int parseComponent (const tstring& str);
template <int N, class Parser, int MinComponentCount=0>
class Base {
public:
enum { ComponentCount = N };
Base() {
memset(components, 0, sizeof(components));
}
private:
bool verifyComponentCount(size_t recognizedComponentCount) const {
return true;
}
protected:
void init(const tstring& str) {
size_t recognizedComponentCount = N;
const size_t unrecognisedChars = Parser()(str, *components,
recognizedComponentCount);
if (unrecognisedChars) {
JP_THROW(tstrings::any()
<< "Failed to parse [" << str << "] version string completely."
<< " Number of unrecognized characters is " << unrecognisedChars);
}
if (recognizedComponentCount < MinComponentCount || !verifyComponentCount(recognizedComponentCount)) {
// Input string is too short.
JP_THROW(tstrings::any() << "Failed to parse [" << str
<< "] version string. The string is too short");
}
strValue = str;
}
public:
const tstring& source() const {
return strValue;
}
bool operator < (const Base& other) const {
for (int i = 0; i < N; ++i) {
const int a = components[i];
const int b = other.components[i];
if (a < b) {
return true;
}
if (b < a) {
return false;
}
}
return false;
}
bool operator <= (const Base& other) const {
return *this == other || *this < other;
}
bool operator > (const Base& other) const {
return ! (*this <= other);
}
bool operator >= (const Base& other) const {
return ! (*this < other);
}
bool operator == (const Base& other) const {
return (0 == memcmp(components, other.components, sizeof(components)));
}
bool operator != (const Base& other) const {
return ! (*this == other);
}
protected:
int components[N];
private:
tstring strValue;
};
} // namespace VersionDetails
template <class Base>
struct Version: public Base {
Version() {
}
explicit Version(const tstring& str) {
Base::init(str);
}
Version(const Base& other): Base(other) {
}
};
#endif // #ifndef Version_h

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2022, 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
@ -23,71 +23,175 @@
* questions.
*/
#include <Windows.h>
#include <msiquery.h>
#include <shlwapi.h>
extern "C" {
#include "MsiDb.h"
#include "MsiCA.h"
#include "Version.h"
#include "FileUtils.h"
#include "WinErrorHandling.h"
#ifdef JP_EXPORT_FUNCTION
#error Unexpected JP_EXPORT_FUNCTION define
#endif
#define JP_EXPORT_FUNCTION comment(linker, "/EXPORT:" __FUNCTION__ "=" __FUNCDNAME__)
BOOL WINAPI DllMain(HINSTANCE hInst, ULONG ulReason,
LPVOID lpvReserved) {
return TRUE;
JP_CA(CheckInstallDir) {
const tstring installDir = ca.getProperty(_T("INSTALLDIR"));
bool canProceed = !FileUtils::isFileExists(installDir);
if (!canProceed && FileUtils::isDirectory(installDir)) {
canProceed = !FileUtils::isDirectoryNotEmpty(installDir);
}
BOOL DirectoryExist(TCHAR *szValue) {
DWORD attr = GetFileAttributes(szValue);
if (attr == INVALID_FILE_ATTRIBUTES) {
return FALSE;
}
ca.setProperty(_T("INSTALLDIR_VALID"), canProceed ? _T("1") : _T("0"));
}
if (attr & FILE_ATTRIBUTE_DIRECTORY) {
return TRUE;
}
return FALSE;
namespace {
typedef Version<
VersionDetails::Base<10, VersionDetails::Parser, 2>
> DottedVersion;
class ProductInfo {
public:
explicit ProductInfo(const Guid& pc): productCode(pc),
version(msi::getProductInfo(pc, INSTALLPROPERTY_VERSIONSTRING)) {
}
UINT __stdcall CheckInstallDir(MSIHANDLE hInstall) {
#pragma JP_EXPORT_FUNCTION
const DottedVersion& getVersion() const {
return version;
}
TCHAR *szValue = NULL;
DWORD cchSize = 0;
const Guid& getProductCode() const {
return productCode;
}
UINT result = MsiGetProperty(hInstall, TEXT("INSTALLDIR"),
(LPTSTR)TEXT(""), &cchSize);
if (result == ERROR_MORE_DATA) {
cchSize = cchSize + 1; // NULL termination
szValue = new TCHAR[cchSize];
if (szValue) {
result = MsiGetProperty(hInstall, TEXT("INSTALLDIR"),
szValue, &cchSize);
} else {
return ERROR_INSTALL_FAILURE;
}
private:
Guid productCode;
DottedVersion version;
};
void findInstalledProducts(const Guid& upgradeCode,
std::vector<ProductInfo>& products) {
const LPCTSTR upgradeCodeStr = upgradeCode.toMsiString().c_str();
for (DWORD productCodeIdx = 0; true; ++productCodeIdx) {
TCHAR productCode[39 /* http://msdn.microsoft.com/en-us/library/aa370101(v=vs.85).aspx */];
const UINT status = MsiEnumRelatedProducts(upgradeCodeStr, 0,
productCodeIdx, productCode);
if (ERROR_NO_MORE_ITEMS == status) {
break;
}
if (result != ERROR_SUCCESS) {
delete [] szValue;
return ERROR_INSTALL_FAILURE;
}
if (DirectoryExist(szValue)) {
if (PathIsDirectoryEmpty(szValue)) {
MsiSetProperty(hInstall, TEXT("INSTALLDIR_VALID"), TEXT("1"));
} else {
MsiSetProperty(hInstall, TEXT("INSTALLDIR_VALID"), TEXT("0"));
}
if (ERROR_SUCCESS == status) {
LOG_TRACE(tstrings::any() << "Found " << productCode << " product");
JP_NO_THROW(products.push_back(ProductInfo(Guid(productCode))));
} else {
MsiSetProperty(hInstall, TEXT("INSTALLDIR_VALID"), TEXT("1"));
LOG_WARNING(tstrings::any()
<< "MsiEnumRelatedProducts("
<< upgradeCodeStr << ", "
<< productCodeIdx
<< ") failed with error=[" << status << "]");
if (ERROR_INVALID_PARAMETER == status) {
break;
}
}
}
}
DottedVersion getDottedVersion(const msi::DatabaseRecord& record, UINT idx) {
if (!MsiRecordIsNull(record.getHandle(), idx)) {
JP_NO_THROW(return DottedVersion(record.getString(idx)));
}
return DottedVersion();
}
bool dbContainsUpgradeTable(const msi::Database &db) {
msi::DatabaseView view(db,
_T("SELECT Name FROM _Tables WHERE Name = 'Upgrade'"));
msi::DatabaseRecord record;
while (!record.tryFetch(view).empty()) {
return true;
}
return false;
}
} // namespace
JP_CA(FindRelatedProductsEx) {
if (ca.isInMode(MSIRUNMODE_MAINTENANCE)) {
// MSI skips standard FindRelatedProducts action in maintenance mode,
// so should we do for custom FindRelatedProducts action
LOG_TRACE("Not run in maintenance mode");
return;
}
const msi::Database db(ca);
if (!dbContainsUpgradeTable(db)) {
LOG_TRACE("The package doesn't contain Upgrade table");
return;
}
const Guid upgradeCode = Guid(ca.getProperty(_T("UpgradeCode")));
std::vector<ProductInfo> installedProducts;
findInstalledProducts(upgradeCode, installedProducts);
bool migratePropRemoved = false;
// https://docs.microsoft.com/en-us/windows/win32/adsi/sql-dialect
msi::DatabaseView view(db, (tstrings::any()
<< _T("SELECT `VersionMin`,`VersionMax`,`Attributes`,`ActionProperty` FROM Upgrade WHERE `ActionProperty` <> NULL And `UpgradeCode` = '")
<< upgradeCode.toMsiString() << _T("'")).tstr());
msi::DatabaseRecord record;
while (!record.tryFetch(view).empty()) {
const tstring actionProperty = record.getString(4);
// Clean up properties set by the standard FindRelatedProducts action
ca.removeProperty(actionProperty);
if (!migratePropRemoved) {
ca.removeProperty(_T("MIGRATE"));
migratePropRemoved = true;
}
const DottedVersion versionMin = getDottedVersion(record, 1);
const DottedVersion versionMax = getDottedVersion(record, 2);
const int attrs = MsiRecordIsNull(
record.getHandle(), 3) ? 0 : record.getInteger(3);
std::vector<ProductInfo>::const_iterator productIt =
installedProducts.begin();
std::vector<ProductInfo>::const_iterator productEnd =
installedProducts.end();
for (; productIt != productEnd; ++productIt) {
bool minMatch;
if (versionMin.source().empty()) {
minMatch = true;
} else if (attrs & msidbUpgradeAttributesVersionMinInclusive) {
minMatch = (versionMin <= productIt->getVersion());
} else {
minMatch = (versionMin < productIt->getVersion());
}
bool maxMatch;
if (versionMax.source().empty()) {
maxMatch = true;
} else if (attrs & msidbUpgradeAttributesVersionMaxInclusive) {
maxMatch = (productIt->getVersion() <= versionMax);
} else {
maxMatch = (productIt->getVersion() < versionMax);
}
if (minMatch && maxMatch) {
tstring value = productIt->getProductCode().toMsiString();
ca.setProperty(actionProperty, value);
ca.setProperty(_T("MIGRATE"), value);
// Bail out after the first match as action
// property has been set already.
// There is no way to communicate multiple product codes
// through a single property.
break;
}
}
delete [] szValue;
return ERROR_SUCCESS;
}
}

View File

@ -331,7 +331,7 @@ public final class PackageTest extends RunnablePackageTest {
return this;
}
public final static class Group extends RunnablePackageTest {
public static class Group extends RunnablePackageTest {
public Group(PackageTest... tests) {
handlers = Stream.of(tests)
.map(PackageTest::createPackageTypeHandlers)

View File

@ -41,7 +41,8 @@ public abstract class RunnablePackageTest {
.filter(Predicate.not(Action.INITIALIZE::equals))
.filter(Predicate.not(Action.FINALIZE::equals))
.collect(Collectors.toList()));
if (hasAction(Action.PURGE) && !actionList.contains(Action.PURGE)) {
if (hasAction(Action.PURGE) && (!actionList.contains(Action.PURGE)
&& actionList.contains(Action.CREATE))) {
// Default action list contains "purge" action meaning
// packages are not needed for further processing.
// Copy this behavior in custom action list.

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2022, 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.
*/
var msi;
if (WScript.Arguments.Count() > 0) {
msi = WScript.Arguments(0)
} else {
var shell = new ActiveXObject('WScript.Shell')
msi = shell.ExpandEnvironmentStrings('%JpMsiFile%')
}
var query = "SELECT `UpgradeCode`, `VersionMin`,`VersionMax`,`Language`,`Attributes`,`Remove`,`ActionProperty` FROM Upgrade WHERE `VersionMax` = NULL"
var installer = new ActiveXObject('WindowsInstaller.Installer');
var database = installer.OpenDatabase(msi, 1)
var view = database.OpenView(query);
view.Execute();
try {
var record = view.Fetch();
record.StringData(2) = '2.0.0.3'
record.IntegerData(5) = 257
view.Modify(6, record)
view.Modify(3, record)
database.Commit();
} finally {
view.Close();
}

View File

@ -0,0 +1,215 @@
/*
* Copyright (c) 2022, 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.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import jdk.jpackage.internal.IOUtils;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.Executor;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.RunnablePackageTest.Action;
import jdk.jpackage.test.TKit;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
/**
* Test --app-version parameter properly supports long version numbers, i.e.
* version numbers with more than three components.
* Output of the test should be WinLongVersionTest-1.0.exe,
* WinLongVersionTest-2.0.0.1.exe, and WinLongVersionTest-2.0.0.2.exe installers.
* The output installers should provide the same functionality as
* the default installer (see description of the default installer in
* SimplePackageTest.java) but have the same product code and different
* versions.
* Test scenario:
* - Run WinLongVersionTest-2.0.0.2.exe;
* - Run WinLongVersionTest-1.0.exe; package installed with
* WinLongVersionTest-2.0.0.2.exe command must remain installed
* - Run WinLongVersionTest-2.0.0.1.exe; packages installed with
* WinLongVersionTest-2.0.0.2.exe and WinLongVersionTest-1.0.exe installers
* must be automatically uninstalled, only WinLongVersionTest-2.0.0.1 package
* must remain installed
* - Uninstall WinLongVersionTest-2.0.0.2; all packages installed in the test
* scenario must be uninstalled
*/
/*
* @test
* @summary jpackage with long version number
* @library ../helpers
* @key jpackagePlatformPackage
* @requires (jpackage.test.SQETest != null)
* @build jdk.jpackage.test.*
* @requires (os.family == "windows")
* @modules jdk.jpackage/jdk.jpackage.internal
* @compile WinLongVersionTest.java
* @run main/othervm/timeout=540 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=WinLongVersionTest.test
*/
/*
* @test
* @summary jpackage with long version number
* @library ../helpers
* @key jpackagePlatformPackage
* @requires (jpackage.test.SQETest == null)
* @build jdk.jpackage.test.*
* @requires (os.family == "windows")
* @modules jdk.jpackage/jdk.jpackage.internal
* @compile WinLongVersionTest.java
* @run main/othervm/timeout=540 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=WinLongVersionTest
*/
public class WinLongVersionTest {
@Test
public static void test() throws IOException {
Supplier<PackageTest> init = () -> {
final UUID upgradeCode = UUID.fromString(
"65099D7A-D5B1-4E5B-85B1-717F0DE4D5D5");
return new PackageTest()
.forTypes(PackageType.WINDOWS)
.configureHelloApp()
.addInitializer(cmd -> cmd.addArguments("--win-upgrade-uuid",
upgradeCode.toString())) ;
};
PackageTest test1 = init.get().addInitializer(cmd -> {
cmd.setArgumentValue("--app-version", "2.0.0.2");
cmd.setArgumentValue("--arguments", "bar");
cmd.setArgumentValue("--install-dir", cmd.name() + "-1");
});
// Tweak Upgrade table of the second package in a way the default
// FindRelatedProducts MSI action will find 1st package, but custom
// jpackage's FindRelatedProductsEx action will not as it gracefuly
// handles more than 3 components of version strings.
// In MSI log of the 2nd package installartion session it will be something like:
/*
Action start 12:08:38: FindRelatedProducts.
FindRelatedProducts: Found application: {D88EEA02-56CC-34AD-8216-C2CC244FA898}
MSI (c) (0C:14) [12:08:38:040]: PROPERTY CHANGE: Adding JP_DOWNGRADABLE_FOUND property. Its value is '{D88EEA02-56CC-34AD-8216-C2CC244FA898}'.
MSI (c) (0C:14) [12:08:38:040]: PROPERTY CHANGE: Adding MIGRATE property. Its value is '{D88EEA02-56CC-34AD-8216-C2CC244FA898}'.
Action ended 12:08:38: FindRelatedProducts. Return value 1.
...
Action start 12:08:38: JpFindRelatedProducts.
Java [12:08:38.180 libwixhelper.cpp:120 (FindRelatedProductsEx)] TRACE: Entering FindRelatedProductsEx
Java [12:08:38.185 libwixhelper.cpp:85 (`anonymous-namespace'::findInstalledPackages)] TRACE: Found {D88EEA02-56CC-34AD-8216-C2CC244FA898} product
Java [12:08:38.187 MsiCA.cpp:61 (msi::CAImpl::removeProperty)] TRACE: Removing MSI property 'JP_UPGRADABLE_FOUND'
Java [12:08:38.187 MsiCA.cpp:61 (msi::CAImpl::removeProperty)] TRACE: Removing MSI property 'MIGRATE'
Java [12:08:38.189 MsiCA.cpp:61 (msi::CAImpl::removeProperty)] TRACE: Removing MSI property 'JP_DOWNGRADABLE_FOUND'
Java [12:08:38.190 libwixhelper.cpp:0 (FindRelatedProductsEx)] TRACE: Exiting FindRelatedProductsEx (entered at libwixhelper.cpp:120)
Action ended 12:08:38: JpFindRelatedProducts. Return value 1.
*/
PackageTest test2 = init.get().addInstallVerifier(cmd -> {
if (!cmd.isPackageUnpacked()) {
// Installation of this package must NOT uninstall
// previously installed package. Verify that.
test1.run(Action.VERIFY_INSTALL);
}
}).forTypes(PackageType.WIN_EXE)
.addInitializer(cmd -> {
final Path resourceDir = TKit.createTempDirectory("resources");
cmd.addArguments("--resource-dir", resourceDir);
Path scriptPath = resourceDir.resolve(String.format(
"%s-post-msi.wsf", cmd.name()));
IOUtils.createXml(scriptPath, xml -> {
xml.writeStartElement("job");
xml.writeAttribute("id", "main");
xml.writeStartElement("script");
xml.writeAttribute("language", "JScript");
xml.writeCData(String.join("\n", Files.readAllLines(
TKit.TEST_SRC_ROOT.resolve(String.format(
"resources/%s-edit-msi.js", cmd.name())))));
xml.writeEndElement();
xml.writeEndElement();
});
}).forTypes(PackageType.WIN_MSI)
.addBundleVerifier(cmd -> {
Executor.of("cscript.exe", "//Nologo")
.addArgument(TKit.TEST_SRC_ROOT.resolve(String.format(
"resources/%s-edit-msi.js", cmd.name())))
.addArgument(cmd.outputBundle())
.execute();
});
// Replace real uninstall commands for the first packages with nop action.
// They will be uninstalled automatically when the last package will
// be installed.
test1.disablePackageUninstaller();
test2.disablePackageUninstaller();
PackageTest test3 = init.get().addInitializer(cmd -> {
cmd.setArgumentValue("--app-version", "2.0.0.1");
cmd.setArgumentValue("--arguments", "buzz");
});
new PackageTest.Group(test1, test2, test3).run();
}
@Test
public static void testNoUpgradeTable() throws IOException {
new PackageTest()
.forTypes(PackageType.WINDOWS)
.configureHelloApp()
.addInitializer(cmd -> {
final Path resourceDir = TKit.createTempDirectory("resources");
cmd.addArguments("--resource-dir", resourceDir);
cmd.setFakeRuntime();
// Create package without Upgrade table
Document doc = IOUtils.initDocumentBuilder().parse(
Files.newInputStream(TKit.SRC_ROOT.resolve(
"windows/classes/jdk/jpackage/internal/resources/main.wxs")));
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xPath.evaluate("/Wix/Product/Upgrade",
doc, XPathConstants.NODESET);
nodes.item(0).getParentNode().removeChild(nodes.item(0));
Source source = new DOMSource(doc);
Result result = new StreamResult(Files.newOutputStream(
resourceDir.resolve("main.wxs")));
Transformer trans = TransformerFactory.newInstance().newTransformer();
trans.transform(source, result);
}).run();
}
}