Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f52b402
Initial commit. Not ready for a PR yet but should be in source contro…
Nov 7, 2025
f338bd6
Splitting up work so that this is only creating a lock file + adding …
Nov 11, 2025
375456b
Adding changelog
Nov 11, 2025
2ba5e1b
Updating test lock files to be multi-line for better json readability
Nov 11, 2025
c530c4c
Printing out lock file values to see why tests are failing
Nov 11, 2025
5ef88ed
Attempt #2 to print out testing values
Nov 11, 2025
e9f5109
Attempt #3
Nov 11, 2025
f7ef50e
Delete all repos prior to lock file tests to avoid conflicts
Nov 11, 2025
80a2015
Adding repository value for the base module on the top level of the l…
Nov 11, 2025
a0eb905
Refactoring to use %JSON.Adaptor for exporting to and creating lock file
Nov 17, 2025
dc4ee30
Adding alias, maxlens to string properties
Dec 1, 2025
6db5567
Adding multiple repo types test
Dec 3, 2025
b837b48
Using XData blocks instead of new repo class + adding data types
Dec 5, 2025
8522c6d
Adding create lock file to load and update in addition to install
Dec 8, 2025
8f1f497
Uninstalling modules in OnAfterOneTest() instead of in each individua…
Dec 9, 2025
6f586e4
Updating changelog to pick up changes from main
Dec 9, 2025
0c53936
middle update
Dec 9, 2025
c2f5185
Re-updating changelog
Dec 9, 2025
4462a47
Removing "enabled" and "deploymentEnabled" fields from repository def…
Dec 10, 2025
22ee017
Merge remote-tracking branch 'origin/main' into lock-file
Dec 10, 2025
49fc412
Moving CreateLockFileForModule() call to where dependency graph was c…
Dec 11, 2025
6b738c7
Merge remote-tracking branch 'origin/main' into lock-file
Dec 11, 2025
170b6c7
Using moniker instead of repeated definition of LockFileTypeGet()
Dec 11, 2025
db18027
More descriptive naming of lock file test modules
Dec 12, 2025
9ec3d48
Adding test class with updated module names
Dec 12, 2025
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 @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #950: Added support for listing installed Python packages using `list -python`, `list -py` and `list-installed -python`
- #822: The CPF resource processor now supports system expressions and macros in CPF merge files
- #578 Added functionality to record and display IPM history of install, uninstall, load, and update
- #961: Adding creation of a lock file for a module by using the `-create-lockfile` flag on install.

### Changed
- #316: All parameters, except developer mode, included with a `load`, `install` or `update` command will be propagated to dependencies
Expand Down
7 changes: 7 additions & 0 deletions src/cls/IPM/DataType/VersionString.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Class %IPM.DataType.VersionString Extends %Library.String [ ClassType = datatype ]
{

/// The maximum number of characters the string can contain.
Parameter MAXLEN As INTEGER = 100;

}
119 changes: 119 additions & 0 deletions src/cls/IPM/General/LockFile.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/// This class is used for interacting with a lock file for a given module
/// Covers functionality for both creating a lock file and installing from a lock file
Copy link
Collaborator

Choose a reason for hiding this comment

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

With regards to separation of concerns, it may be better for this class to just be the interface for interacting with the lock file and have the creation/install methods in a utils class?

Class %IPM.General.LockFile Extends (%RegisteredObject, %JSON.Adaptor)
{

/// Name of the JSON lock file
Parameter LockFileName As String = "module-lock.json";

/// Name of the module the lock file is for
Property ModuleName As %IPM.DataType.ModuleName(%JSONFIELDNAME = "name") [ Required ];

/// Version string of the module
Property VersionString As %IPM.DataType.VersionString(%JSONFIELDNAME = "version") [ Required ];

/// Repository where the base module used to create the lock file comes from
Property Repository As %IPM.DataType.RepoName(%JSONFIELDNAME = "repository");

/// Version of the lock file schema used in creating the lock file
Property LockFileVersion As %String(%JSONFIELDNAME = "lockFileVersion", MAXLEN = "") [ InitialExpression = "1" ];

/// Array of repository definitions from where at least one of the base module or dependency comes from
Property Repositories As array Of %IPM.Repo.Definition(%JSONFIELDNAME = "repositories");

/// Array of dependency modules required by the module. Ordered by least dependent to most dependent
Property Dependencies As array Of %IPM.General.LockFile.Dependency(%JSONFIELDNAME = "dependencies");

/// When given the name of a module, creates the lock file for it and saves it to module.Root_..#LockFileName
/// flatDependencyList is the output from ##class(%IPM.Utils.Module).GetFlatDependencyListFromInvertedDependencyGraph()
ClassMethod CreateLockFileForModule(
module As %IPM.Storage.Module,
ByRef flatDependencyList,
ByRef params)
{
set verbose = $get(params("Verbose"), 0)
if (verbose) {
write !, "Creating lock file for module "_module.DisplayName
}

// Create lock file object and set values for the base module
set lockFile = ##class(%IPM.General.LockFile).%New()
set lockFile.ModuleName = module.Name
set lockFile.VersionString = module.VersionString
set lockFile.Repository = module.Repository

// Iterate over module dependency list
// For each module:
// 1. Set values for module name, version, repository (name), and direct dependencies
// 2. Add repository information to the list (if the module is from a new repo)
set moduleName = ""
for i = 1:1:flatDependencyList.Count() {
set moduleName = flatDependencyList.GetAt(i).Name
set mod = ##class(%IPM.Storage.Module).NameOpen(moduleName, 0, .sc)
$$$ThrowOnError(sc)

if (verbose) {
write !, "Adding "_mod.DisplayName_" to the lock file"
}

// Add the dependency to the lock file
set dependencyVal = ##class(%IPM.General.LockFile.Dependency).%New()
set dependencyVal.VersionString = mod.VersionString
set dependencyVal.Repository = mod.Repository
set depKey = ""
for {
set depMod = mod.Dependencies.GetNext(.depKey)
quit:depKey=""
$$$ThrowOnError(dependencyVal.Dependencies.SetAt(depMod.VersionString, depMod.Name))
}
$$$ThrowOnError(lockFile.Dependencies.SetAt(dependencyVal, mod.Name))

// Add the dependency's repository to the lock file
do AddRepositoryToLockFile(.lockFile, mod.Repository, verbose)
}
// Add repository for base module if not already added by a dependency
// Skip undefined repositories as that means the module was installed via the zpm "load" command
if (module.Repository '= "") {
do AddRepositoryToLockFile(.lockFile, module.Repository, verbose)
}

$$$ThrowOnError(lockFile.%JSONExportToStream(.lockFileJSON, "LockFileMapping"))

set lockFilePath = module.Root_..#LockFileName
if (verbose) {
write !, "Saving lock file for "_module.Name_" to: "_lockFilePath
}
set file = ##class(%Stream.FileCharacter).%New()
$$$ThrowOnError(file.LinkToFile(lockFilePath))
$$$ThrowOnError(file.CopyFrom(lockFileJSON))
$$$ThrowOnError(file.%Save())
}

/// Adds a module's repository to the lock file
ClassMethod AddRepositoryToLockFile(
ByRef lockFile As %IPM.General.LockFile,
repositoryName As %String,
verbose As %Boolean = 0) [ Internal ]
{
set repo = ..GetRepo(repositoryName)
if 'lockFile.Repositories.IsDefined(repo.Name) {
if (verbose) {
write !, "Adding new repository to the lock file: "_repo.Name
}
$$$ThrowOnError(lockFile.Repositories.SetAt(repo, repo.Name))
}
}

/// Returns a repo based on its name
ClassMethod GetRepo(repoName As %String) As %IPM.Repo.Definition [ Internal ]
{
if ##class(%IPM.Repo.Definition).ServerDefinitionKeyExists(repoName, .id) {
set repo = ##class(%IPM.Repo.Definition).%OpenId(id, , .sc)
$$$ThrowOnError(sc)
return repo
} else {
$$$ThrowStatus($$$ERROR($$$GeneralError,$$$FormatText("Tried getting repo ""%1"" but none found with that name", repoName)))
}
}

}
21 changes: 21 additions & 0 deletions src/cls/IPM/General/LockFile/Dependency.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// Class which defines the schema to be used for a dependency object within a lock file
Class %IPM.General.LockFile.Dependency Extends (%RegisteredObject, %JSON.Adaptor)
{

/// Version string for this dependency module
Property VersionString As %IPM.DataType.VersionString(%JSONFIELDNAME = "version");

/// Name of the repository this module comes from
Property Repository As %IPM.DataType.RepoName(%JSONFIELDNAME = "repository");

/// Array of transient dependencies required by this dependency module.
/// key: module name
/// value: semantic version expression (required by this module)
///
/// Example: {
/// "moduleOne": "^1.0.0",
/// "moduleTwo": "^2.1.3"
/// }
Property Dependencies As array Of %IPM.DataType.VersionString(%JSONFIELDNAME = "dependencies");

}
3 changes: 3 additions & 0 deletions src/cls/IPM/Main.cls
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ This command is an alias for `module-action module-name publish`
<modifier name="env" aliases="e" dataAlias="EnvFiles" value="true" description="Semicolon separated paths to the environment files in json format." />
<modifier name="path" aliases="p" value="true" description="Location of local tarball containing the updated version of the module. Overrides 'version' parameter if present." />
<modifier name="dev" dataAlias="DeveloperMode" dataValue="1" description="Sets the DeveloperMode flag for the module's lifecycle. Key consequences of this are that ^Sources will be configured for resources in the module, and installer methods will be called with the dev mode flag set." />
<modifier name="create-lockfile" aliases="lock" dataAlias="CreateLockFile" dataValue="1" description="Upon update, creates/updates the module's lock file." />
</command>

<command name="makedeployed">
Expand Down Expand Up @@ -290,6 +291,7 @@ load C:\module\root\path -env C:\path\to\env1.json;C:\path\to\env2.json
<modifier name="extra-pip-flags" dataAlias="ExtraPipFlags" value="true" description="Extra flags to pass to pip when installing python dependencies. Surround the flags (and values) with quotes if spaces are present. Default flags are &quot;--target &lt;target&gt; --python-version &lt;pyversion&gt; --only-binary=:all:&quot;." />
<modifier name="synchronous" value="false" deprecated="true" description="DEPRECATED. Dependencies are now always loaded synchronously with independent lifecycle phases doing their own multi-threading as needed." />
<modifier name="force" aliases="f" value="false" description="Allows the user to load a newer version of an existing module without running update steps." />
<modifier name="create-lockfile" aliases="lock" dataAlias="CreateLockFile" dataValue="1" description="Upon load, creates/updates the module's lock file." />

<!-- Parameters -->
<parameter name="path" required="true" description="Directory on the local filesystem, containing a file named module.xml" />
Expand Down Expand Up @@ -416,6 +418,7 @@ install -env /path/to/env1.json;/path/to/env2.json example-package
<modifier name="extra-pip-flags" dataAlias="ExtraPipFlags" value="true" description="Extra flags to pass to pip when installing python dependencies. Surround the flags (and values) with quotes if spaces are present. Default flags are &quot;--target &lt;target&gt; --python-version &lt;pyversion&gt; --only-binary=:all:&quot;."/>
<modifier name="synchronous" value="false" deprecated="true" description="DEPRECATED. Dependencies are now always loaded synchronously with independent lifecycle phases doing their own multi-threading as needed." />
<modifier name="force" aliases="f" value="false" description="Allows the user to install a newer version of an existing module without running update steps." />
<modifier name="create-lockfile" aliases="lock" dataAlias="CreateLockFile" dataValue="1" description="Upon install, creates/updates the module's lock file." />

</command>

Expand Down
10 changes: 9 additions & 1 deletion src/cls/IPM/Repo/Definition.cls
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Include (%syPrompt, %IPM.Common)

Class %IPM.Repo.Definition Extends (%Persistent, %ZEN.DataModel.Adaptor, %IPM.CLI.Commands) [ Abstract ]
Class %IPM.Repo.Definition Extends (%Persistent, %ZEN.DataModel.Adaptor, %IPM.CLI.Commands, %JSON.Adaptor) [ Abstract ]
{

Parameter DEFAULTGLOBAL = "^IPM.Repo.Definition";
Expand All @@ -21,6 +21,9 @@ Parameter MaxDisplayTabCount As INTEGER = 3;

Index ServerDefinitionKey On Name [ Unique ];

/// String "type" identifier used in lock files
Property LockFileType As %String(MAXLEN = 100) [ Calculated, ReadOnly ];

Property Name As %IPM.DataType.RepoName [ Required ];

Property Enabled As %Boolean [ InitialExpression = 1 ];
Expand Down Expand Up @@ -280,6 +283,11 @@ ClassMethod GetOne(
quit ""
}

Method LockFileTypeGet()
{
return ..#MONIKER
}

Storage Default
{
<Data name="RepoDefinitionDefaultData">
Expand Down
11 changes: 11 additions & 0 deletions src/cls/IPM/Repo/Filesystem/Definition.cls
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,17 @@ ClassMethod ScanDirectory(
quit tSC
}

XData LockFileMapping
{
<Mapping xmlns="http://www.intersystems.com/jsonmapping">
<Property Name="LockFileType" FieldName="type" />
<Property Name="OverriddenSortOrder" FieldName="overriddenSortOrder" />
<Property Name="ReadOnly" FieldName="readOnly" />
<Property Name="Root" FieldName="root" />
<Property Name="Depth" FieldName="depth" />
</Mapping>
}

Storage Default
{
<Data name="FilesystemRepoDefinitionDefaultData">
Expand Down
11 changes: 11 additions & 0 deletions src/cls/IPM/Repo/Oras/Definition.cls
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ Method GetPublishingManager(ByRef status)
return ##class(%IPM.Repo.Oras.PublishManager).%Get(.status)
}

XData LockFileMapping
{
<Mapping xmlns="http://www.intersystems.com/jsonmapping">
<Property Name="LockFileType" FieldName="type" />
<Property Name="OverriddenSortOrder" FieldName="overriddenSortOrder" />
<Property Name="ReadOnly" FieldName="readOnly" />
<Property Name="URL" FieldName="url" />
<Property Name="Namespace" FieldName="orasNamespace" />
</Mapping>
}

Storage Default
{
<Data name="OrasRepoDefinitionDefaultData">
Expand Down
15 changes: 15 additions & 0 deletions src/cls/IPM/Repo/Remote/Definition.cls
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@ Method GetPublishingManager(ByRef status)
return ##class(%IPM.Repo.Remote.PublishManager).%Get(.status)
}

Method LockFileTypeGet()
{
return ..#MONIKERALIAS
}

XData LockFileMapping
{
<Mapping xmlns="http://www.intersystems.com/jsonmapping">
<Property Name="LockFileType" FieldName="type" />
<Property Name="OverriddenSortOrder" FieldName="overriddenSortOrder" />
<Property Name="ReadOnly" FieldName="readOnly" />
<Property Name="URL" FieldName="url" />
</Mapping>
}

Storage Default
{
<Data name="RemoteRepoDefinitionDefaultData">
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 @@ -11,7 +11,7 @@ Index Name On Name [ Unique ];

Property GlobalScope As %Boolean;

Property VersionString As %String(MAXLEN = 100, XMLNAME = "Version") [ InitialExpression = "0.0.1+snapshot", Required ];
Property VersionString As %IPM.DataType.VersionString(XMLNAME = "Version") [ InitialExpression = "0.0.1+snapshot", Required ];

/// Does not need comparison method to be code generated because that comparing <property>VersionString</property> is good enough.
Property Version As %IPM.General.SemanticVersion(ForceCodeGenerate = 0, XMLPROJECTION = "NONE") [ Required ];
Expand Down
13 changes: 13 additions & 0 deletions src/cls/IPM/Utils/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,10 @@ ClassMethod LoadNewModule(
$$$ThrowOnError(##class(%IPM.Storage.Module).CheckSystemRequirements(tModuleName))

kill params("ModuleName") // Ensure module name is not passed down to dependency loads
// If lock file is to be created, add the module name to params so lock file is just created for base module and not any of the dependency modules loaded
if $get(params("CreateLockFile"), 0) && '$data(params("LockFileModule")){
set params("LockFileModule") = tModule.Name
}
do ..LoadDependencies(tModule, .params)

set tSC = $system.OBJ.Load(pDirectory_"module.xml",$select(tVerbose:"d",1:"-d"),,.tLoadedList)
Expand Down Expand Up @@ -1254,6 +1258,15 @@ ClassMethod LoadDependencies(
set sc = ..LoadModuleReference(moduleReference.ServerName, moduleReference.Name, moduleReference.VersionString,$get(tDeployed), $get(tPlatformVersion), .pParams)
$$$ThrowOnError(sc)
}

// Create lock file if specified for this module
if $get(pParams("CreateLockFile"), 0) && (pModule.Name = $get(pParams("LockFileModule"))) {
try {
do ##class(%IPM.General.LockFile).CreateLockFileForModule(pModule, flatDepList, .tParams)
} catch (ex) {
write !, $$$FormatText("Error creating lock file for %1 - %2", pModule.Name, ex.DisplayString()), !
}
}
}

/// Construct an inverted dependency graph from the dependency graph of a module. <br />
Expand Down
Loading