Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #1112: Packaging a module with a globals resource now respects SourcesRoot, placing the exported file at the correct path in the tarball
- #1057: Fix IPM not cleaning up after itself on self-uninstall
- #1122: Packaging should recognize resources in dependency modules set to deploy
- #1119: The update command should check version requirements using post-update values instead of what's currently installed

## [0.10.6] - 2026-02-24

Expand Down
2 changes: 1 addition & 1 deletion src/cls/IPM/Storage/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1109,7 +1109,7 @@ Method ProcessSingleDependencyIterative(
}

set sc = ##class(%IPM.Utils.Module).GetRequiredVersionExpression(
pDep.Name,otherDepsList,.installedReqExpr,.installedConstraintList
pDep.Name,otherDepsList,,.installedReqExpr,.installedConstraintList
)
$$$ThrowOnError(sc)
set searchExpr = searchExpr.And(installedReqExpr)
Expand Down
89 changes: 66 additions & 23 deletions src/cls/IPM/Utils/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ ClassMethod LoadModuleReference(

// Ensure requested versions match those required by other modules in the namespace, excluding versions currently being installed
// (the requirements of such modules are already known to be satisfied)
set tSC = ..GetRequiredVersionExpression(pModuleName,,.tExpression,.tSourceList)
set tSC = ..GetRequiredVersionExpression(pModuleName,,.pDependencyGraph,.tExpression,.tSourceList)
if $$$ISERR(tSC) {
quit
}
Expand Down Expand Up @@ -349,36 +349,79 @@ ClassMethod GetModuleObjectFromString(

/// Returns a semantic version expression capturing all version requirements for a given module name in the current namespace.
/// A list of modules to exclude may be provided (for example, if these modules would be updated at the same time).
Comment thread
isc-jlechtne marked this conversation as resolved.
///
/// If provided a dependency graph, will use versions defined there instead of what the SQL call returns.
/// This is especially important for the update command, where we want to check requirements with post-update versions as opposed to what's currently installed.
ClassMethod GetRequiredVersionExpression(
pModuleName As %String,
pExcludeModules As %List = "",
Output pExpression As %IPM.General.SemanticVersionExpression,
Output pSourceList As %List) As %Status
moduleName As %String,
excludeModules As %List = "",
ByRef dependencyGraph,
Output expression As %IPM.General.SemanticVersionExpression,
Output sourceList As %List) As %Status
{
set tSC = $$$OK
set sc = $$$OK
try {
set pExpression = ##class(%IPM.General.SemanticVersionExpression).%New()
set pSourceList = ""
// Add modules from the dependency graph to the excludeModules list
if ($data(dependencyGraph)) {
do ..ConstructInvertedDependencyGraph(.dependencyGraph, .invertedDependencyGraph)
set flatDepList = ..GetFlatDependencyListFromInvertedDependencyGraph(.invertedDependencyGraph)
for i = 1:1:flatDepList.Count() {
set excludeModules = excludeModules _ $listbuild(flatDepList.GetAt(i).Name)
}
}

set tResult = ##class(%IPM.Storage.Module).VersionRequirementsFunc(pModuleName,pExcludeModules)
if (tResult.%SQLCODE < 0) {
$$$ThrowStatus($$$ERROR($$$SQLCode,tResult.%SQLCODE,tResult.%Message))
set expression = ##class(%IPM.General.SemanticVersionExpression).%New()
set sourceList = ""

set result = ##class(%IPM.Storage.Module).VersionRequirementsFunc(moduleName,excludeModules)
if (result.%SQLCODE < 0) {
$$$ThrowStatus($$$ERROR($$$SQLCode,result.%SQLCODE,result.%Message))
}

while tResult.%Next(.tSC) {
$$$ThrowOnError(tSC)
set tVersion = tResult.%Get("Version")
$$$ThrowOnError(##class(%IPM.General.SemanticVersionExpression).FromString(tVersion,.tVersionExpr))
set pExpression = pExpression.And(tVersionExpr)
set pSourceList = pSourceList_$listbuild($listtostring(tResult.%Get("ModuleNames"),", ")_": "_tVersion)
while result.%Next(.sc) {
$$$ThrowOnError(sc)
set version = result.%Get("Version")
$$$ThrowOnError(##class(%IPM.General.SemanticVersionExpression).FromString(version,.versionExpr))
set expression = expression.And(versionExpr)
set sourceList = sourceList_$listbuild($listtostring(result.%Get("ModuleNames"),", ")_": "_version)
}
$$$ThrowOnError(sc)

// Add modules from the dependency graph to the expression and sourceList objects
if ($data(dependencyGraph)) {
set key = ""
for {
set key = $order(dependencyGraph(moduleName, key))
if (key = "") {
quit
}
// Name of module which requires this one + the version expression string it requires
set requiringModuleName = $piece(key, " ")
set version = dependencyGraph(moduleName, key)

// Iterate over sourceList; if the required version expression is equivalent to this one, add this module name to that version
set newVersion = 1
for i=1:1:$listlength(sourceList) {
if $find($listget(sourceList, i), version) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is is possible that $find is too loose of a search? ie. would there be a case where the version incorrectly matches only a substring?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The syntax of one element in sourceList looks like "module-a, module-b: ^1.0.0" for a case where both module-a and module-b require version ^1.0.0 of whatever module is being evaluated (the case here being adding some module-x to this string if it also requires ^1.0.0) so there won't be a case of this incorrectly passing since that would require a module name to have the identical version string in it.

Could alternatively do $piece($listget(sourceList, i), " ", *) = version as a way to completely isolate the version expression but splitting up the string on the delimiter should be unnecessary for this case.

set $list(sourceList, i) = requiringModuleName _ ", " _ $list(sourceList, i)
set newVersion = 0
quit
}
}
// If this is a new required version expression, add a new item to the list
if (newVersion) {
$$$ThrowOnError(##class(%IPM.General.SemanticVersionExpression).FromString(version, .versionExpr))
set expression = expression.And(versionExpr)
set sourceList = sourceList _ $listbuild(requiringModuleName _ ": " _ version)
}
}
}
$$$ThrowOnError(tSC)
Comment thread
isc-jlechtne marked this conversation as resolved.
} catch e {
set pExpression = $$$NULLOREF
set pSourceList = ""
set tSC = e.AsStatus()
set expression = $$$NULLOREF
set sourceList = ""
set sc = e.AsStatus()
}
quit tSC
quit sc
}

/// Returns a flat list of dependents for a given module name (and optional version) <br />
Expand Down Expand Up @@ -1278,7 +1321,7 @@ ClassMethod LoadDependencies(
// Ignore modules already installed that do not need to be installed again
continue
}
set sc = ..LoadModuleReference(moduleReference.ServerName, moduleReference.Name, moduleReference.VersionString, moduleReference.Deployed, moduleReference.PlatformVersion, .pParams)
set sc = ..LoadModuleReference(moduleReference.ServerName, moduleReference.Name, moduleReference.VersionString, moduleReference.Deployed, moduleReference.PlatformVersion, .pParams, .dependencyGraph)
$$$ThrowOnError(sc)
}

Expand Down
61 changes: 61 additions & 0 deletions tests/integration_tests/Test/PM/Integration/Update.cls
Original file line number Diff line number Diff line change
Expand Up @@ -662,4 +662,65 @@ Method TestDevModePropagation()
do $$$AssertEquals(dep.DeveloperMode, 0)
}

/// Test case to make sure that update doesn't fail due to version errors that get resolved as a part of update
/// i.e. - Module A 2.0.0 requires Module B ^1.0.0
/// - Module A 2.1.0 requires Module B ^2.0.0
/// - Pre-Update: Module A 2.0.0, Module B 1.0.0
/// - Post-Update: Module A 2.1.0, Module B 2.0.0
/// - Don't want a version error to be thrown in flight when Module A is 2.1.0 and Module B is still 1.0.0
Method TestDependencyVersionsUpdated()
{
// Define variables for this test
set modA = "module-a"
set modB = "module-b"
set modC = "module-c"
set modD = "module-d"

set modAPreVersion = "2.0.0"
set modBPreVersion = "1.0.0"
set modCPreVersion = "5.6.45+snapshot"

set modAPostVersion = "2.1.0+snapshot"
set modBPostVersion = "2.0.0+snapshot"
set modCPostVersion = "6.2.0+snapshot"

// Install base module module-a
set sc = ##class(%IPM.Main).Shell("install -v " _ modA _ " " _ modAPreVersion)
do $$$AssertStatusOK(sc, "Successfully installed " _ modA _ " " _ modAPreVersion)

// Confirm that module-a and dependencies were installed with the correct versions
set mod = ##class(%IPM.Storage.Module).NameOpen(modA)
do $$$AssertEquals(mod.Version.ToString(), modAPreVersion, "Module " _ modA _ " correctly installed as " _ modAPreVersion)
set mod = ##class(%IPM.Storage.Module).NameOpen(modB)
do $$$AssertEquals(mod.Version.ToString(), modBPreVersion, "Module " _ modB _ " correctly installed as " _ modBPreVersion)
set mod = ##class(%IPM.Storage.Module).NameOpen(modC)
do $$$AssertEquals(mod.Version.ToString(), modCPreVersion, "Module " _ modC _ " correctly installed as " _ modCPreVersion)

// Install module-d prior to update
set sc = ##class(%IPM.Main).Shell("install -v " _ modD)
do $$$AssertStatusOK(sc, "Successfully installed " _ modD)

// Update base module module-a. Update should error due to version incompatabilities caused by module-d
set sc = ##class(%IPM.Main).Shell("update -v " _ modA _ " " _ modAPostVersion)
do $$$AssertStatusNotOK(sc, "Updating " _ modA _ " fails due to version errors with previously installed " _ modD)

// Uninstall module-d then try the update again
set sc = ##class(%IPM.Main).Shell("uninstall -v " _ modD)
do $$$AssertStatusOK(sc, "Successfully uninstalled " _ modD)
set sc = ##class(%IPM.Main).Shell("update -v " _ modA _ " " _ modAPostVersion)
do $$$AssertStatusOK(sc, "Successfully updated " _ modA _ " and its dependencies")

// Confirm that module-a and dependencies were updated to the correct versions
set mod = ##class(%IPM.Storage.Module).NameOpen(modA)
do $$$AssertEquals(mod.Version.ToString(), modAPostVersion, "Module " _ modA _ " correctly installed as " _ modAPostVersion)
set mod = ##class(%IPM.Storage.Module).NameOpen(modB)
do $$$AssertEquals(mod.Version.ToString(), modBPostVersion, "Module " _ modB _ " correctly installed as " _ modBPostVersion)
set mod = ##class(%IPM.Storage.Module).NameOpen(modC)
do $$$AssertEquals(mod.Version.ToString(), modCPostVersion, "Module " _ modC _ " correctly installed as " _ modCPostVersion)

// Uninstall module-a and dependencies
set sc = ##class(%IPM.Main).Shell("uninstall -r " _ modA)
do $$$AssertStatusOK(sc, "Successfully uninstalled " _ modA _ " and dependencies")
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Class ModuleA.Class1
{

ClassMethod MethodA()
{
write !, "This is ##class(ModuleA.Class1).MethodA()"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="module-a.ZPM">
<Module>
<Name>module-a</Name>
<Version>2.1.0+snapshot</Version>
<Resource Name="ModuleA.PKG" />
<Dependencies>
<ModuleReference>
<Name>module-b</Name>
<Version>^2.0.0</Version>
</ModuleReference>
</Dependencies>
</Module>
</Document>
</Export>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Class ModuleB.Class1
{

ClassMethod MethodA()
{
write !, "This is ##class(ModuleB.Class1).MethodA()"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="module-b.ZPM">
<Module>
<Name>module-b</Name>
<Version>2.0.0+snapshot</Version>
<Resource Name="ModuleB.PKG" />
<Dependencies>
<ModuleReference>
<Name>module-c</Name>
<Version>^6.1.15</Version>
</ModuleReference>
</Dependencies>
</Module>
</Document>
</Export>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Class ModuleC.Class1
{

ClassMethod MethodA()
{
write !, "This is ##class(ModuleC.Class1).MethodA()"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="module-c.ZPM">
<Module>
<Name>module-c</Name>
<Version>6.2.0+snapshot</Version>
<Resource Name="ModuleC.PKG" />
</Module>
</Document>
</Export>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Class ModuleA.Class1
{

ClassMethod MethodA()
{
write !, "This is ##class(ModuleA.Class1).MethodA()"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="module-a.ZPM">
<Module>
<Name>module-a</Name>
<Version>2.0.0</Version>
<Resource Name="ModuleA.PKG" />
<Dependencies>
<ModuleReference>
<Name>module-b</Name>
<Version>1.0.0</Version>
</ModuleReference>
</Dependencies>
</Module>
</Document>
</Export>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Class ModuleB.Class1
{

ClassMethod MethodA()
{
write !, "This is ##class(ModuleB.Class1).MethodA()"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="module-b.ZPM">
<Module>
<Name>module-b</Name>
<Version>1.0.0</Version>
<Resource Name="ModuleB.PKG" />
<Dependencies>
<ModuleReference>
<Name>module-c</Name>
<Version>^5.4.1</Version>
</ModuleReference>
</Dependencies>
</Module>
</Document>
</Export>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Class ModuleC.Class1
{

ClassMethod MethodA()
{
write !, "This is ##class(ModuleC.Class1).MethodA()"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="module-c.ZPM">
<Module>
<Name>module-c</Name>
<Version>5.6.45+snapshot</Version>
<Resource Name="ModuleC.PKG" />
</Module>
</Document>
</Export>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Class ModuleD.Class1
{

ClassMethod MethodA()
{
write !, "This is ##class(ModuleD.Class1).MethodA()"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="module-d.ZPM">
<Module>
<Name>module-d</Name>
<Version>1.0.0</Version>
<Resource Name="ModuleD.PKG" />
<Dependencies>
<ModuleReference>
<Name>module-c</Name>
<Version>^5.3.0</Version>
</ModuleReference>
</Dependencies>
</Module>
</Document>
</Export>
Loading