Use AppsAndFeaturesEntries DisplayVersion info for installed package version mapping#2213
Use AppsAndFeaturesEntries DisplayVersion info for installed package version mapping#2213yao-msft merged 20 commits intomicrosoft:masterfrom
Conversation
@check-spelling-bot ReportUnrecognized words, please review:
Previously acknowledged words that are now absentactivatable amd Archs dsc enr FWW Globals hackathon lww mytool OSVERSION Packagedx parametermap symlink Uninitialize WDAG whatif wsbTo accept these unrecognized words as correct (and remove the previously acknowledged and now absent words), run the following commands... in a clone of the git@github.com:yao-msft/winget-cli.git repository |
| bool IsUpdateVersionApplicable(const Utility::Version& installedVersion, const Utility::Version& updateVersion) | ||
| { | ||
| return (installedVersion < updateVersion || updateVersion.IsLatest()); | ||
| return (installedVersion < updateVersion || (installedVersion.IsLatest() && updateVersion.IsLatest())); |
There was a problem hiding this comment.
Why does the installed version need to be Latest in order for the update version of Latest to be applicable? #Resolved
|
|
||
| for (auto const& installer : Installers) | ||
| { | ||
| if (DoesInstallerTypeWriteAppsAndFeaturesEntry(installer.InstallerType) && installer.InstallerType != InstallerTypeEnum::Portable) |
There was a problem hiding this comment.
I would like another semantic function name here to encode the removal of portable, possibly something like DoesInstallerTypeSupportVersionRange.
Alternately, we should probably just move to a full on installer traits type to capture this information and require that all new installer types must have had this thought through. #Resolved
| bool IsApproximateVersion() const { return m_approximateComparator != ApproximateComparator::None; } | ||
|
|
||
| // Static methods to create an approximate version from a base version. | ||
| static Version CreateLessThanApproximateVersion(const Version& base); |
There was a problem hiding this comment.
Why not add an ApproximateComparator parameter to the Version constructor? I think specifically Version(const Version&, ApproximateComparator) is what you would need. #Resolved
| if (CaseInsensitiveStartsWith(m_version, s_Approximate_Less_Than)) | ||
| { | ||
| m_approximateComparator = ApproximateComparator::LessThan; | ||
| baseVersion = m_version.substr(2, m_version.length() - 2); |
There was a problem hiding this comment.
| baseVersion = m_version.substr(2, m_version.length() - 2); | |
| baseVersion = m_version.substr(s_Approximate_Less_Than.length(), m_version.length() - s_Approximate_Less_Than.length()); | |
| ``` #Resolved |
|
|
||
| bool Version::operator<(const Version& other) const | ||
| { | ||
| // If base versions are same, result is based on approximate comparator |
There was a problem hiding this comment.
This seems like a very costly way of doing the comparison (copies and doing full equality comparison first). Why not just move the approximate comparison resolution to the end when we have decided that the base is equal? Yes, it means refactoring the flow a bit to not return on equality, but it seems feasible.
For performance it would probably even be good to move the latest/unknown to be determined at Assign, which would remove the need to create temporaries here. Then the refactored flow would be able to be cleaner. #Resolved
| std::shared_ptr<IPackage> m_availablePackage; | ||
| Source m_trackingSource; | ||
| std::shared_ptr<IPackage> m_trackingPackage; | ||
| mutable std::atomic_bool m_isOverrideInstalledVersionInitialized = false; |
There was a problem hiding this comment.
Why not just initialize it in the constructor; I don't see a lot of cases where the value is going to be ignored. #Resolved
| std::initializer_list<SQLite::Builder::QualifiedColumn> columns, | ||
| std::initializer_list<std::string_view> manifestColumnNames) | ||
| { | ||
| THROW_HR_IF(E_UNEXPECTED, manifestColumnNames.size() != 0 && manifestColumnNames.size() != columns.size()); |
There was a problem hiding this comment.
| THROW_HR_IF(E_UNEXPECTED, manifestColumnNames.size() != 0 && manifestColumnNames.size() != columns.size()); | |
| THROW_HR_IF(E_UNEXPECTED, manifestColumnNames.size() != columns.size()); |
I don't think this code works if manifestColumnNames is not provided along with columns. #Resolved
|
|
||
| DEFINE_ENUM_FLAG_OPERATORS(WinGetValidateManifestResult); | ||
|
|
||
| WINGET_UTIL_API WinGetValidateManifestV3( |
There was a problem hiding this comment.
Add result of discussion WinGetUtil.h...
Should we make this a little more complicated from the number of APIs, but less complicated for each?
Imagine:
WinGetCreateManifest(path, message, merge path, option, manifest handle);
WinGetValidateManifestV3(manifest handle, index, result, message, operation, option);
WinGetCloseManifest(manifest handle);
The create is basically V2 validate, but we return a pointer to reuse for future validations. It performs the direct manifest validations only. The returned pointer saves continually parsing the manifest and enables efficient splitting of the various validations we perform. Then the V3 validate can be "perform this one validation". #Resolved
| // Check arp version ranges do not overlap | ||
| if (result && sqliteIndex->GetVersion() >= Schema::Version{ 1, 5 }) | ||
| { | ||
| result = ValidateArpVersionConsistency(sqliteIndex); |
There was a problem hiding this comment.
Why is this not part of CheckConsistency? #Resolved
| // Only validate against json schema | ||
| SchemaValidation = 0x1, | ||
| // Validate against schema and also perform semantic validation | ||
| SchemaAndSemanticValidation = 0x2, |
There was a problem hiding this comment.
Shouldn't this just be SemanticValidation and you pass both flags to get both? #Pending
| const char* const ExceededCommandsLimit = "Only zero or one value for Commands may be specified for InstallerType portable."; | ||
| const char* const ScopeNotSupported = "Scope is not supported for InstallerType portable."; | ||
| const char* const ApproximateVersionNotAllowed = "Approximate version not allowed."; | ||
| const char* const ArpVersionOverlapWithIndex = "DisplayVersion declared in the manifest has overlap with existing DisplayVersion range in the index. Existing DisplayVersion range in index: "; |
There was a problem hiding this comment.
This got me thinking - Are version ranges architecture specific, and/or should they be? I could see a scenario where a publisher uses the following -
PackageVersion: 1.0-Alpha # This is their marketing version
Installers:
- Architecture: x86
AppsAndFeaturesEntries:
- DisplayVersion: 1.0.4.0
- Architecture: x64
AppsAndFeaturesEntries:
- DisplayVersion: 1.0.5.0According to the spec, this would result in an Version Range of [1.0.4.0, 1.0.5.0]
Suppose they release a new update -
PackageVersion: 1.1-Alpha # This is their marketing version
Installers:
- Architecture: x86
AppsAndFeaturesEntries:
- DisplayVersion: 1.0.4.1
- Architecture: x64
AppsAndFeaturesEntries:
- DisplayVersion: 1.0.5.1According to the logic, wouldn't 1.0.4.1 overlap with [1.0.4.0, 1.0.5.0]. since 1.0.4.0 =< 1.0.4.1 =< 1.0.5.0, thereby triggering a false overlap warning? #ByDesign
There was a problem hiding this comment.
Correct, this will be treated as unsupported case according to the spec previously written. Since there's no restriction on what is written to DisplayVersion entry, an installer could also write different values for different locale. The above example can be extended to scope, installertype, etc. Essentially this will end up being each installer is in its own set.
| const char* const ScopeNotSupported = "Scope is not supported for InstallerType portable."; | ||
| const char* const ApproximateVersionNotAllowed = "Approximate version not allowed."; | ||
| const char* const ArpVersionOverlapWithIndex = "DisplayVersion declared in the manifest has overlap with existing DisplayVersion range in the index. Existing DisplayVersion range in index: "; | ||
| const char* const ArpVersionValidationInternalError = "Encountered internal error during DisplayVersion validation against index."; |
There was a problem hiding this comment.
| const char* const ArpVersionValidationInternalError = "Encountered internal error during DisplayVersion validation against index."; | |
| const char* const ArpVersionValidationInternalError = "Internal error while validating DisplayVersion against index."; |
Nit: Passive voice #Resolved
| Version(std::string&& version, std::string_view splitChars = DefaultSplitChars); | ||
|
|
||
| // Constructing an approximate version from a base version. | ||
| Version(const Version& baseVersion, ApproximateComparator approximateComparator); |
There was a problem hiding this comment.
| Version(const Version& baseVersion, ApproximateComparator approximateComparator); | |
| Version(Version baseVersion, ApproximateComparator approximateComparator); |
And move its contents. This gives us both copies and moves. #Resolved
|
|
||
| // All parts tested were equal, so this is only less if there are more parts in other. | ||
| return m_parts.size() < other.m_parts.size(); | ||
| // All parts tested were equal |
There was a problem hiding this comment.
You have to change the Latest and Unknown comparisons to only return in the cases that they are not equal so that they can fall down here. Currently they return false in the equal case, since this is operator<. #Resolved
There was a problem hiding this comment.
yep, and unit tests failed for this... rushing to get a commit run in the pipeline before running tests locally
| for (auto const& tuple : rawVersionValues) | ||
| { | ||
| auto& [version, arpMin, arpMax] = tuple; | ||
| if (!arpMin.empty() && !arpMax.empty() && arpMin != version && arpMax != version) |
There was a problem hiding this comment.
| if (!arpMin.empty() && !arpMax.empty() && arpMin != version && arpMax != version) | |
| if (!arpMin.empty() && !arpMax.empty() && (arpMin != version || arpMax != version)) |
It is ok if one of them is the package version.
Also, why not perform the test in the loop while building the strings out? #Resolved
| std::vector<std::pair<std::string, Utility::VersionRange>> arpVersionMap; | ||
| // Stores raw versions value strings to run a preliminary check whether version mapping is needed. | ||
| std::vector<std::tuple<std::string, std::string, std::string>> rawVersionValues; | ||
| auto versionKeys = availablePackage->GetAvailableVersionKeys(); |
There was a problem hiding this comment.
I've just realized that if the available package is a composite of multiple sources, then this is flawed. Do we do that now or not? #Pending
There was a problem hiding this comment.
I think composite available package may need more thinking, currently we stop when we find one from a source, I'll add a comment as a reminder when we enable composite available package one day
| bool result = m_interface->CheckConsistency(m_dbconn, log); | ||
|
|
||
| // Check arp version ranges do not overlap | ||
| if ((result || log) && GetVersion() >= Schema::Version{ 1, 5 }) |
There was a problem hiding this comment.
I think that this should be done in the versioned code. Why is it not? #Resolved
| WINGET_UTIL_API WinGetCreateManifest( | ||
| WINGET_STRING inputPath, | ||
| BOOL* succeeded, | ||
| WINGET_SQLITE_MANIFEST_HANDLE* manifest, |
There was a problem hiding this comment.
| WINGET_SQLITE_MANIFEST_HANDLE* manifest, | |
| WINGET_MANIFEST_HANDLE* manifest, | |
| ``` #Resolved |
| if (WI_IsFlagSet(option, WinGetValidateManifestOptionV2::SchemaValidation) || WI_IsFlagSet(option, WinGetValidateManifestOptionV2::SchemaAndSemanticValidation)) | ||
| Manifest* manifestPtr = reinterpret_cast<Manifest*>(manifest); | ||
| SQLiteIndex* sqliteIndex = nullptr; | ||
| if (index) |
There was a problem hiding this comment.
You can just reinterpret_cast without the null check. #Resolved
| // a string representing validation errors if validation failed. | ||
| // If mergedManifestPath is provided, this method will write a merged manifest | ||
| // to the location specified by mergedManifestPath | ||
| WINGET_UTIL_API WinGetValidateManifestV2( |
There was a problem hiding this comment.
| WINGET_UTIL_API WinGetValidateManifestV2( | |
| WINGET_UTIL_API WinGetValidateManifestV3( |
And change the parameters to match. #Resolved
| WINGET_UTIL_API WinGetCloseManifest( | ||
| WINGET_MANIFEST_HANDLE manifest); | ||
|
|
||
| // Validates a given manifest. Returns a bool for validation result and |
| installerType == InstallerTypeEnum::Nullsoft || | ||
| installerType == InstallerTypeEnum::Wix || | ||
| installerType == InstallerTypeEnum::Burn | ||
| ); |
There was a problem hiding this comment.
Is there any advantage to using ||'s here, instead of a switch statement? I would think that when compiled, a switch statement would be slightly more efficient, given that it compiles as a jump table rather than a sequential evaluation
There was a problem hiding this comment.
I just copied from above similar functions. I guess it's more readable. In the end, this function will be invoked at most once for each installed package so I think I'll leave it for consistency for now.
| m_maxVersion = std::move(maxVersion); | ||
| } | ||
|
|
||
| bool VersionRange::HasOverlapWith(const VersionRange& other) const |
There was a problem hiding this comment.
| bool VersionRange::HasOverlapWith(const VersionRange& other) const | |
| bool VersionRange::Overlaps(const VersionRange& other) const |
Why use many word, when few word do trick?
| const std::vector<Part>& GetParts() const { return m_parts; } | ||
|
|
||
| // Returns if the version is an approximate version. | ||
| bool IsApproximateVersion() const { return m_approximateComparator != ApproximateComparator::None; } |
There was a problem hiding this comment.
| bool IsApproximateVersion() const { return m_approximateComparator != ApproximateComparator::None; } | |
| bool IsApproximate() const { return m_approximateComparator != ApproximateComparator::None; } |
Why use many word, when few word do trick?
| bool thisIsUnknown = IsBaseVersionUnknown(); | ||
| bool otherIsUnknown = other.IsBaseVersionUnknown(); |
There was a problem hiding this comment.
Is there a reason to change this, since IsUnknown() calls IsBaseVersionUnkown()
There was a problem hiding this comment.
In a previous commit, approximate to Unknown is actually supported. I disabled it after internal discussion about what we would do to Unknown. I kept it for parity. And in case we need it in the future.
| return m_minVersion <= other.m_maxVersion && m_maxVersion >= other.m_minVersion; | ||
| } | ||
|
|
||
| bool VersionRange::IsSameAsSingleVersion(const Version& version) const |
There was a problem hiding this comment.
It seems like this could be an operator overload of == instead of a separate method?
There was a problem hiding this comment.
I don't feel like doing == for different objects, which may cause confusion IMO.
For #980
Changes
Described in spec
Validation
Added unit and e2e tests. Did a local manual validation.
Microsoft Reviewers: Open in CodeFlow