Skip to content

Commit a6aadec

Browse files
authored
Package Manager should support creation of a lock file (#963)
Added support for creating a lock file during installation of modules via a flag. Note: this doesn't include usage of the lock file for installs (to be done in future PR).
1 parent 4cace76 commit a6aadec

File tree

65 files changed

+1695
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1695
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- #950: Added support for listing installed Python packages using `list -python`, `list -py` and `list-installed -python`
1515
- #822: The CPF resource processor now supports system expressions and macros in CPF merge files
1616
- #578 Added functionality to record and display IPM history of install, uninstall, load, and update
17+
- #961: Adding creation of a lock file for a module by using the `-create-lockfile` flag on install.
1718

1819
### Changed
1920
- #316: All parameters, except developer mode, included with a `load`, `install` or `update` command will be propagated to dependencies
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Class %IPM.DataType.VersionString Extends %Library.String [ ClassType = datatype ]
2+
{
3+
4+
/// The maximum number of characters the string can contain.
5+
Parameter MAXLEN As INTEGER = 100;
6+
7+
}

src/cls/IPM/General/LockFile.cls

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/// This class is used for interacting with a lock file for a given module
2+
/// Covers functionality for both creating a lock file and installing from a lock file
3+
Class %IPM.General.LockFile Extends (%RegisteredObject, %JSON.Adaptor)
4+
{
5+
6+
/// Name of the JSON lock file
7+
Parameter LockFileName As String = "module-lock.json";
8+
9+
/// Name of the module the lock file is for
10+
Property ModuleName As %IPM.DataType.ModuleName(%JSONFIELDNAME = "name") [ Required ];
11+
12+
/// Version string of the module
13+
Property VersionString As %IPM.DataType.VersionString(%JSONFIELDNAME = "version") [ Required ];
14+
15+
/// Repository where the base module used to create the lock file comes from
16+
Property Repository As %IPM.DataType.RepoName(%JSONFIELDNAME = "repository");
17+
18+
/// Version of the lock file schema used in creating the lock file
19+
Property LockFileVersion As %String(%JSONFIELDNAME = "lockFileVersion", MAXLEN = "") [ InitialExpression = "1" ];
20+
21+
/// Array of repository definitions from where at least one of the base module or dependency comes from
22+
Property Repositories As array Of %IPM.Repo.Definition(%JSONFIELDNAME = "repositories");
23+
24+
/// Array of dependency modules required by the module. Ordered by least dependent to most dependent
25+
Property Dependencies As array Of %IPM.General.LockFile.Dependency(%JSONFIELDNAME = "dependencies");
26+
27+
/// When given the name of a module, creates the lock file for it and saves it to module.Root_..#LockFileName
28+
/// flatDependencyList is the output from ##class(%IPM.Utils.Module).GetFlatDependencyListFromInvertedDependencyGraph()
29+
ClassMethod CreateLockFileForModule(
30+
module As %IPM.Storage.Module,
31+
ByRef flatDependencyList,
32+
ByRef params)
33+
{
34+
set verbose = $get(params("Verbose"), 0)
35+
if (verbose) {
36+
write !, "Creating lock file for module "_module.DisplayName
37+
}
38+
39+
// Create lock file object and set values for the base module
40+
set lockFile = ##class(%IPM.General.LockFile).%New()
41+
set lockFile.ModuleName = module.Name
42+
set lockFile.VersionString = module.VersionString
43+
set lockFile.Repository = module.Repository
44+
45+
// Iterate over module dependency list
46+
// For each module:
47+
// 1. Set values for module name, version, repository (name), and direct dependencies
48+
// 2. Add repository information to the list (if the module is from a new repo)
49+
set moduleName = ""
50+
for i = 1:1:flatDependencyList.Count() {
51+
set moduleName = flatDependencyList.GetAt(i).Name
52+
set mod = ##class(%IPM.Storage.Module).NameOpen(moduleName, 0, .sc)
53+
$$$ThrowOnError(sc)
54+
55+
if (verbose) {
56+
write !, "Adding "_mod.DisplayName_" to the lock file"
57+
}
58+
59+
// Add the dependency to the lock file
60+
set dependencyVal = ##class(%IPM.General.LockFile.Dependency).%New()
61+
set dependencyVal.VersionString = mod.VersionString
62+
set dependencyVal.Repository = mod.Repository
63+
set depKey = ""
64+
for {
65+
set depMod = mod.Dependencies.GetNext(.depKey)
66+
quit:depKey=""
67+
$$$ThrowOnError(dependencyVal.Dependencies.SetAt(depMod.VersionString, depMod.Name))
68+
}
69+
$$$ThrowOnError(lockFile.Dependencies.SetAt(dependencyVal, mod.Name))
70+
71+
// Add the dependency's repository to the lock file
72+
do AddRepositoryToLockFile(.lockFile, mod.Repository, verbose)
73+
}
74+
// Add repository for base module if not already added by a dependency
75+
// Skip undefined repositories as that means the module was installed via the zpm "load" command
76+
if (module.Repository '= "") {
77+
do AddRepositoryToLockFile(.lockFile, module.Repository, verbose)
78+
}
79+
80+
$$$ThrowOnError(lockFile.%JSONExportToStream(.lockFileJSON, "LockFileMapping"))
81+
82+
set lockFilePath = module.Root_..#LockFileName
83+
if (verbose) {
84+
write !, "Saving lock file for "_module.Name_" to: "_lockFilePath
85+
}
86+
set file = ##class(%Stream.FileCharacter).%New()
87+
$$$ThrowOnError(file.LinkToFile(lockFilePath))
88+
$$$ThrowOnError(file.CopyFrom(lockFileJSON))
89+
$$$ThrowOnError(file.%Save())
90+
}
91+
92+
/// Adds a module's repository to the lock file
93+
ClassMethod AddRepositoryToLockFile(
94+
ByRef lockFile As %IPM.General.LockFile,
95+
repositoryName As %String,
96+
verbose As %Boolean = 0) [ Internal ]
97+
{
98+
set repo = ..GetRepo(repositoryName)
99+
if 'lockFile.Repositories.IsDefined(repo.Name) {
100+
if (verbose) {
101+
write !, "Adding new repository to the lock file: "_repo.Name
102+
}
103+
$$$ThrowOnError(lockFile.Repositories.SetAt(repo, repo.Name))
104+
}
105+
}
106+
107+
/// Returns a repo based on its name
108+
ClassMethod GetRepo(repoName As %String) As %IPM.Repo.Definition [ Internal ]
109+
{
110+
if ##class(%IPM.Repo.Definition).ServerDefinitionKeyExists(repoName, .id) {
111+
set repo = ##class(%IPM.Repo.Definition).%OpenId(id, , .sc)
112+
$$$ThrowOnError(sc)
113+
return repo
114+
} else {
115+
$$$ThrowStatus($$$ERROR($$$GeneralError,$$$FormatText("Tried getting repo ""%1"" but none found with that name", repoName)))
116+
}
117+
}
118+
119+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// Class which defines the schema to be used for a dependency object within a lock file
2+
Class %IPM.General.LockFile.Dependency Extends (%RegisteredObject, %JSON.Adaptor)
3+
{
4+
5+
/// Version string for this dependency module
6+
Property VersionString As %IPM.DataType.VersionString(%JSONFIELDNAME = "version");
7+
8+
/// Name of the repository this module comes from
9+
Property Repository As %IPM.DataType.RepoName(%JSONFIELDNAME = "repository");
10+
11+
/// Array of transient dependencies required by this dependency module.
12+
/// key: module name
13+
/// value: semantic version expression (required by this module)
14+
///
15+
/// Example: {
16+
/// "moduleOne": "^1.0.0",
17+
/// "moduleTwo": "^2.1.3"
18+
/// }
19+
Property Dependencies As array Of %IPM.DataType.VersionString(%JSONFIELDNAME = "dependencies");
20+
21+
}

src/cls/IPM/Main.cls

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ This command is an alias for `module-action module-name publish`
163163
<modifier name="env" aliases="e" dataAlias="EnvFiles" value="true" description="Semicolon separated paths to the environment files in json format." />
164164
<modifier name="path" aliases="p" value="true" description="Location of local tarball containing the updated version of the module. Overrides 'version' parameter if present." />
165165
<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." />
166+
<modifier name="create-lockfile" aliases="lock" dataAlias="CreateLockFile" dataValue="1" description="Upon update, creates/updates the module's lock file." />
166167
</command>
167168

168169
<command name="makedeployed">
@@ -290,6 +291,7 @@ load C:\module\root\path -env C:\path\to\env1.json;C:\path\to\env2.json
290291
<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;." />
291292
<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." />
292293
<modifier name="force" aliases="f" value="false" description="Allows the user to load a newer version of an existing module without running update steps." />
294+
<modifier name="create-lockfile" aliases="lock" dataAlias="CreateLockFile" dataValue="1" description="Upon load, creates/updates the module's lock file." />
293295

294296
<!-- Parameters -->
295297
<parameter name="path" required="true" description="Directory on the local filesystem, containing a file named module.xml" />
@@ -416,6 +418,7 @@ install -env /path/to/env1.json;/path/to/env2.json example-package
416418
<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;."/>
417419
<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." />
418420
<modifier name="force" aliases="f" value="false" description="Allows the user to install a newer version of an existing module without running update steps." />
421+
<modifier name="create-lockfile" aliases="lock" dataAlias="CreateLockFile" dataValue="1" description="Upon install, creates/updates the module's lock file." />
419422

420423
</command>
421424

src/cls/IPM/Repo/Definition.cls

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Include (%syPrompt, %IPM.Common)
22

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

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

2222
Index ServerDefinitionKey On Name [ Unique ];
2323

24+
/// String "type" identifier used in lock files
25+
Property LockFileType As %String(MAXLEN = 100) [ Calculated, ReadOnly ];
26+
2427
Property Name As %IPM.DataType.RepoName [ Required ];
2528

2629
Property Enabled As %Boolean [ InitialExpression = 1 ];
@@ -280,6 +283,11 @@ ClassMethod GetOne(
280283
quit ""
281284
}
282285

286+
Method LockFileTypeGet()
287+
{
288+
return ..#MONIKER
289+
}
290+
283291
Storage Default
284292
{
285293
<Data name="RepoDefinitionDefaultData">

src/cls/IPM/Repo/Filesystem/Definition.cls

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,17 @@ ClassMethod ScanDirectory(
357357
quit tSC
358358
}
359359

360+
XData LockFileMapping
361+
{
362+
<Mapping xmlns="http://www.intersystems.com/jsonmapping">
363+
<Property Name="LockFileType" FieldName="type" />
364+
<Property Name="OverriddenSortOrder" FieldName="overriddenSortOrder" />
365+
<Property Name="ReadOnly" FieldName="readOnly" />
366+
<Property Name="Root" FieldName="root" />
367+
<Property Name="Depth" FieldName="depth" />
368+
</Mapping>
369+
}
370+
360371
Storage Default
361372
{
362373
<Data name="FilesystemRepoDefinitionDefaultData">

src/cls/IPM/Repo/Oras/Definition.cls

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,17 @@ Method GetPublishingManager(ByRef status)
140140
return ##class(%IPM.Repo.Oras.PublishManager).%Get(.status)
141141
}
142142

143+
XData LockFileMapping
144+
{
145+
<Mapping xmlns="http://www.intersystems.com/jsonmapping">
146+
<Property Name="LockFileType" FieldName="type" />
147+
<Property Name="OverriddenSortOrder" FieldName="overriddenSortOrder" />
148+
<Property Name="ReadOnly" FieldName="readOnly" />
149+
<Property Name="URL" FieldName="url" />
150+
<Property Name="Namespace" FieldName="orasNamespace" />
151+
</Mapping>
152+
}
153+
143154
Storage Default
144155
{
145156
<Data name="OrasRepoDefinitionDefaultData">

src/cls/IPM/Repo/Remote/Definition.cls

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,21 @@ Method GetPublishingManager(ByRef status)
132132
return ##class(%IPM.Repo.Remote.PublishManager).%Get(.status)
133133
}
134134

135+
Method LockFileTypeGet()
136+
{
137+
return ..#MONIKERALIAS
138+
}
139+
140+
XData LockFileMapping
141+
{
142+
<Mapping xmlns="http://www.intersystems.com/jsonmapping">
143+
<Property Name="LockFileType" FieldName="type" />
144+
<Property Name="OverriddenSortOrder" FieldName="overriddenSortOrder" />
145+
<Property Name="ReadOnly" FieldName="readOnly" />
146+
<Property Name="URL" FieldName="url" />
147+
</Mapping>
148+
}
149+
135150
Storage Default
136151
{
137152
<Data name="RemoteRepoDefinitionDefaultData">

src/cls/IPM/Storage/Module.cls

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Index Name On Name [ Unique ];
1111

1212
Property GlobalScope As %Boolean;
1313

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

1616
/// Does not need comparison method to be code generated because that comparing <property>VersionString</property> is good enough.
1717
Property Version As %IPM.General.SemanticVersion(ForceCodeGenerate = 0, XMLPROJECTION = "NONE") [ Required ];

0 commit comments

Comments
 (0)