Skip to content

Commit f2c631f

Browse files
authored
Merge pull request #703 from intersystems/v0.10.x-feat-init-cpf-merge
Feat: CPF Merge
2 parents 39502db + e2af5aa commit f2c631f

File tree

20 files changed

+352
-41
lines changed

20 files changed

+352
-41
lines changed

.github/workflows/main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ jobs:
190190
CONTAINER=$(docker run --network zpm --rm -d ${{ steps.image.outputs.name }} ${{ steps.image.outputs.flags }})
191191
docker cp tests/migration/v0.7-to-v0.9/. $CONTAINER:/tmp/test-package/
192192
sleep 5; docker exec $CONTAINER /usr/irissys/dev/Cloud/ICM/waitISC.sh
193-
docker exec -i $CONTAINER iris session iris -UUSER << EOF
193+
docker exec -i $CONTAINER iris session iris -UUSER << 'EOF'
194194
s version="0.7.4" s r=##class(%Net.HttpRequest).%New(),r.Server="pm.community.intersystems.com",r.SSLConfiguration="ISC.FeatureTracker.SSL.Config" d r.Get("/packages/zpm/"_version_"/installer"),$system.OBJ.LoadStream(r.HttpResponse.Data,"c")
195195
zpm "list":1
196196
zpm "install dsw":1

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- #609 Added support for `-export-deps` when running the "Package" phase of lifecycle
1616
- #541 Added support for ORAS repository
1717
- #704 Added support for passing in env files via `-env /path/to/env1.json;/path/to/env2.json` syntax
18+
- #702 Added a new lifecycle phase `Initialize` which is used for preload
19+
- #702 Added a `<CPF/>` resource, which can be used for CPF merge before/after a specified lifecycle phase or in a custom lifecycle phase.
1820
- #716 Added support to publish under external name by passing `-use-external-name` or `-use-ext`.
1921

2022
### Changed
21-
-
23+
- #702 Preload now happens as part of the new `Initialize` lifecycle phase. `zpm "<module> reload -only"` will no longer auto compile resources in `/preload` directory.
2224

2325
### Fixed
2426
- #474: When loading a .tgz/.tar.gz package, automatically locate the top-most module.xml in case there is nested directory structure (e.g., GitHub releases)
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Class %IPM.DataType.CustomPhaseName Extends %Library.String [ ClassType = datatype ]
2+
{
3+
4+
/// The maximum number of characters the string can contain.
5+
Parameter MAXLEN As INTEGER = 255;
6+
7+
}

src/cls/IPM/DataType/PhaseName.cls

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Class %IPM.DataType.PhaseName Extends %Library.String [ ClassType = datatype ]
2+
{
3+
4+
/// The maximum number of characters the string can contain.
5+
Parameter MAXLEN As INTEGER = 50;
6+
7+
/// Used for enumerated (multiple-choice) attributes.
8+
/// <var>VALUELIST</var> is either a null string ("") or a delimiter
9+
/// separated list (where the delimiter is the first character) of logical values.
10+
/// If a non-null value is present, then the attribute is restricted to values
11+
/// in the list, and the validation code simply checks to see if the value is in the list.
12+
Parameter VALUELIST = ",Clean,Initialize,Reload,*,Validate,ExportData,Compile,Activate,Document,MakeDeployed,Test,Package,Verify,Publish,Configure,Unconfigure";
13+
14+
}

src/cls/IPM/DataType/PhaseWhen.cls

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Class %IPM.DataType.PhaseWhen Extends %Library.String [ ClassType = datatype ]
2+
{
3+
4+
/// The maximum number of characters the string can contain.
5+
Parameter MAXLEN As INTEGER = 50;
6+
7+
/// Used for enumerated (multiple-choice) attributes.
8+
/// <var>VALUELIST</var> is either a null string ("") or a delimiter
9+
/// separated list (where the delimiter is the first character) of logical values.
10+
/// If a non-null value is present, then the attribute is restricted to values
11+
/// in the list, and the validation code simply checks to see if the value is in the list.
12+
Parameter VALUELIST = ",Before,After";
13+
14+
}
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Class %IPM.DataType.ResourceDirectory Extends %Library.String [ ClassType = datatype ]
2+
{
3+
4+
Parameter MAXLEN = 255;
5+
6+
/// Tests if the logical value <var>%val</var>, which is a string, is valid.
7+
/// The validation is based on the class parameter settings used for the class attribute this data type is associated with.
8+
/// In this case, <a href="#MINLEN">MINLEN</a>, <a href="#MAXLEN">MAXLEN</a>, <a href="#VALUELIST">VALUELIST</a>, and <a href="#PATTERN">PATTERN</a>.
9+
ClassMethod IsValid(%val As %RawString) As %Status [ ServerOnly = 0 ]
10+
{
11+
If $Extract(%val) = "/" {
12+
Return $$$ERROR($$$GeneralError, "Resource directory cannot start with a slash.")
13+
}
14+
Set segments = $ListFromString(%val, "/")
15+
Set ptr = 0
16+
While $ListNext(segments, ptr, seg) {
17+
If seg = ".." {
18+
Return $$$ERROR($$$GeneralError, "For security reasons, resource directory cannot contain '..'.")
19+
}
20+
}
21+
Return $$$OK
22+
}
23+
24+
}

src/cls/IPM/Lifecycle/Base.cls

+42-21
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Property PhaseList As %List;
99

1010
/// $ListBuild list of phases in this lifecycle. <br />
1111
/// For each phase name, an instance method named "%<phase name>" must be defined in the class with a return type of %Status.
12-
Parameter PHASES = {$ListBuild("Clean","Reload","*","Validate","ExportData","Compile","Activate","Document","MakeDeployed","Test","Package","Verify", "Publish", "Configure","Unconfigure")};
12+
Parameter PHASES = {$ListBuild("Clean","Initialize", "Reload","*","Validate","ExportData","Compile","Activate","Document","MakeDeployed","Test","Package","Verify", "Publish", "Configure","Unconfigure")};
1313

1414
Property Payload As %Stream.Object [ Private ];
1515

@@ -121,7 +121,7 @@ ClassMethod GetDefaultResourceProcessorProc(pLifecycleClass As %Dictionary.Class
121121
ClassMethod GetCompletePhases(pPhases As %List) As %List
122122
{
123123
// If there is only phase, and the phase is not found in standard phases, it is a custom phase. Return as-is.
124-
If ($ListLength(pPhases) = 1) && ('$ListFind(..#PHASES, $List(pPhases,1))) {
124+
If ($ListLength(pPhases) = 1) && ('$ListFind(..#PHASES, ..MatchSinglePhase($List(pPhases,1)))) {
125125
Return pPhases
126126
}
127127
For i=1:1:$ListLength(pPhases) {
@@ -142,18 +142,19 @@ ClassMethod GetCompletePhasesForOne(pOnePhase As %String) As %List
142142

143143
Quit $Case(pOnePhase,
144144
"clean": $ListBuild("Clean"),
145-
"reload": $ListBuild("Reload","*"),
146-
"validate": $ListBuild("Reload","*","Validate"),
145+
"initialize": $ListBuild("Initialize"),
146+
"reload": $ListBuild("Initialize","Reload","*"),
147+
"validate": $ListBuild("Initialize","Reload","*","Validate"),
147148
"exportdata": $ListBuild("ExportData"),
148-
"compile": $ListBuild("Reload","*","Validate","Compile"),
149-
"activate": $ListBuild("Reload","*","Validate","Compile","Activate"),
149+
"compile": $ListBuild("Initialize","Reload","*","Validate","Compile"),
150+
"activate": $ListBuild("Initialize","Reload","*","Validate","Compile","Activate"),
150151
"document": $ListBuild("Document"),
151152
"makedeployed": $ListBuild("MakeDeployed"),
152-
"test": $ListBuild("Reload","*","Validate","Compile","Activate","Test"),
153-
"package": $ListBuild("Reload","*","Validate","Compile","Activate","Package"),
154-
"verify": $ListBuild("Reload","*","Validate","Compile","Activate","Package","Verify"),
155-
"register": $ListBuild("Reload","*","Validate","Compile","Activate","Package","Register"),
156-
"publish": $ListBuild("Reload","*","Validate","Compile","Activate","Package","Register","Publish"),
153+
"test": $ListBuild("Initialize","Reload","*","Validate","Compile","Activate","Test"),
154+
"package": $ListBuild("Initialize","Reload","*","Validate","Compile","Activate","Package"),
155+
"verify": $ListBuild("Initialize","Reload","*","Validate","Compile","Activate","Package","Verify"),
156+
"register": $ListBuild("Initialize","Reload","*","Validate","Compile","Activate","Package","Register"),
157+
"publish": $ListBuild("Initialize","Reload","*","Validate","Compile","Activate","Package","Register","Publish"),
157158
"configure": $ListBuild("Configure"),
158159
"unconfigure": $ListBuild("Unconfigure"),
159160
: ""
@@ -166,6 +167,7 @@ ClassMethod MatchSinglePhase(pOnePhase As %String) As %String
166167
Set phase = $ZCONVERT(pOnePhase, "L")
167168
Quit $Case(phase,
168169
"clean": "Clean",
170+
"initialize": "Initialize",
169171
"reload": "Reload",
170172
"validate": "Validate",
171173
"exportdata": "ExportData",
@@ -505,6 +507,35 @@ Method %Unconfigure(ByRef pParams) As %Status
505507
Quit tSC
506508
}
507509

510+
Method %Initialize(ByRef pParams) As %Status
511+
{
512+
Set status = $$$OK
513+
Try {
514+
Set key = ""
515+
For {
516+
Set resource = ..Module.Resources.GetNext(.key)
517+
Quit:key=""
518+
If $IsObject(resource.Processor) {
519+
Set status = $$$ADDSC(status,resource.Processor.OnPhase("Initialize",.pParams))
520+
}
521+
}
522+
523+
Set preloadRoot = $Get(pParams("RootDirectory"))_"preload"
524+
Set verbose = $Get(pParams("Verbose"))
525+
If ##class(%File).DirectoryExists(preloadRoot) {
526+
Set tSC = $System.OBJ.ImportDir(preloadRoot, "*", $Select(verbose:"d",1:"-d")_$Select($Tlevel:"/multicompile=0", 1: ""), , 1, .tImported)
527+
If $$$ISERR(tSC) { Quit }
528+
Set tSC = ##class(%IPM.Utils.LegacyCompat).UpdateSuperclassAndCompile(.tImported)
529+
If $$$ISERR(tSC) { Quit }
530+
} ElseIf verbose {
531+
Write !,"Skipping preload - directory does not exist."
532+
}
533+
} Catch ex {
534+
Set status = ex.AsStatus()
535+
}
536+
Quit status
537+
}
538+
508539
Method %Reload(ByRef pParams) As %Status
509540
{
510541
Set tSC = $$$OK
@@ -581,16 +612,6 @@ Method %Reload(ByRef pParams) As %Status
581612

582613
$$$ThrowOnError(..InstallPythonRequirements(tRoot, .pParams))
583614

584-
Set tPreloadRoot = tRoot_"preload"
585-
If ##class(%File).DirectoryExists(tPreloadRoot) {
586-
Set tSC = $System.OBJ.ImportDir(tPreloadRoot, "*", $Select(tVerbose:"d",1:"-d")_$Select($Tlevel:"/multicompile=0", 1: ""), , 1, .tImported)
587-
If $$$ISERR(tSC) { Quit }
588-
Set tSC = ##class(%IPM.Utils.LegacyCompat).UpdateSuperclassAndCompile(.tImported)
589-
If $$$ISERR(tSC) { Quit }
590-
} ElseIf tVerbose {
591-
Write !,"Skipping preload - directory does not exist."
592-
}
593-
594615
// Reload the module definition
595616
Set tSC = ..Module.%Reload()
596617
If $$$ISERR(tSC) {

src/cls/IPM/ResourceProcessor/Abstract.cls

-1
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,6 @@ Method GetSourceControlInfo(Output pInfo As %IPM.ExtensionBase.SourceControl.Res
231231
Quit $$$OK
232232
}
233233

234-
235234
/// Evaluates an expression in a provided string. <br />
236235
/// These special expressions are case-insensitive. <br />
237236
/// Current valid expressions:

src/cls/IPM/ResourceProcessor/CPF.cls

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
Include %IPM.Formatting
2+
3+
Class %IPM.ResourceProcessor.CPF Extends (%IPM.ResourceProcessor.Abstract, %IPM.ResourceProcessor.CustomPhaseMixin)
4+
{
5+
6+
/// Comma-separated list of resource attribute names that this processor uses
7+
Parameter ATTRIBUTES As STRING = "Name,Directory,Phase,CustomPhase,When";
8+
9+
/// Description of resource processor class (shown in UI)
10+
Parameter DESCRIPTION As STRING = "Merges the specified CPF file in the specified lifecycle phase (""Initialize"" by default).";
11+
12+
/// Directory containing the CPF file to merge
13+
Property Directory As %IPM.DataType.ResourceDirectory [ InitialExpression = "cpf" ];
14+
15+
/// FileN ame of the CPF merge file
16+
Property Name As %IPM.DataType.ResourceName [ Required ];
17+
18+
/// The phase before/after which the CPF file should be merged
19+
Property Phase As %IPM.DataType.PhaseName [ InitialExpression = "Initialize" ];
20+
21+
/// When to merge the CPF file: Before or After the specified phase. This only applies to the standard phases.
22+
Property When As %IPM.DataType.PhaseWhen [ InitialExpression = "Before" ];
23+
24+
Method OnBeforePhase(pPhase As %String, ByRef pParams) As %Status
25+
{
26+
If (..When = "Before") && (..Phase = pPhase) && (..CustomPhase = "") {
27+
Quit ..DoMerge(.pParams)
28+
}
29+
Quit $$$OK
30+
}
31+
32+
Method OnAfterPhase(pPhase As %String, ByRef pParams) As %Status
33+
{
34+
If (..When = "After") && (..Phase = pPhase) && (..CustomPhase = "") {
35+
Quit ..DoMerge(.pParams)
36+
}
37+
Quit $$$OK
38+
}
39+
40+
Method OnCustomPhase(pCustomPhase As %String, ByRef pParams) As %Status
41+
{
42+
If (..CustomPhase = pCustomPhase) {
43+
Quit ..DoMerge(.pParams)
44+
}
45+
Quit $$$OK
46+
}
47+
48+
Method DoMerge(ByRef pParams) As %Status
49+
{
50+
Try {
51+
Set verbose = $GET(pParams("Verbose"))
52+
Set root = ..ResourceReference.Module.Root
53+
Set sourcesRoot = ..ResourceReference.Module.SourcesRoot
54+
// Use Construct first, rather than NormalizeFilename, so we don't have to deal with leading/trailing slashes
55+
Set dir = $SELECT($$$isWINDOWS: $REPLACE(..Directory, "/", "\"), 1: ..Directory)
56+
Set dir = ##class(%File).Construct(root, sourcesRoot, dir)
57+
Set filename = ##class(%File).NormalizeFilename(..Name, dir)
58+
If (filename = "") || ('##class(%File).Exists(filename)) {
59+
$$$ThrowStatus($$$ERROR($$$GeneralError, $$$FormatText("CPF file '%1' not found in directory '%2'", ..Name, dir)))
60+
}
61+
62+
Set stream = ##class(%Stream.FileCharacter).%New()
63+
$$$ThrowOnError(stream.LinkToFile(filename))
64+
If verbose {
65+
Write !, "Merging CPF file: ", filename, !
66+
Do stream.OutputToDevice()
67+
}
68+
Do ..MergeCPF(filename)
69+
} Catch ex {
70+
Return ex.AsStatus()
71+
}
72+
Return $$$OK
73+
}
74+
75+
ClassMethod MergeCPF(file As %String)
76+
{
77+
// TODO The $zf(-100) callout is much slower than ##class(Config.CPF).Merge()
78+
// Figure out why ##class(Config.CPF).Merge() doesn't work
79+
// c.f. https://github.com/intersystems/ipm/pull/703#discussion_r1917290136
80+
81+
Set args($INCREMENT(args)) = "merge"
82+
Set args($INCREMENT(args)) = ##class(%SYS.System).GetInstanceName()
83+
Set args($INCREMENT(args)) = file
84+
85+
// Somehow, if the STDOUT is not set, the merge will silently fail
86+
Set flags = "/SHELL/LOGCMD/STDOUT=""zf100stdout""/STDERR=""zf100stderr"""
87+
Set status = $ZF(-100, flags, "iris", .args)
88+
If status '= 0 {
89+
$$$ThrowStatus($$$ERROR($$$GeneralError, "Error merging CPF file. $zf(-100) exited with "_status))
90+
}
91+
}
92+
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// Some processors may need to do something custom when a custom phase is executed.
2+
/// Such as the CPF processor, which optionally merges a CPF file during a custom phase.
3+
/// This mixin provides the CustomPhase property and the OnCustomPhase method.
4+
Class %IPM.ResourceProcessor.CustomPhaseMixin
5+
{
6+
7+
Property CustomPhase As %IPM.DataType.CustomPhaseName;
8+
9+
Method OnCustomPhase(pCustomPhase As %String, ByRef pParams) As %Status
10+
{
11+
Quit $$$OK
12+
}
13+
14+
}

src/cls/IPM/Storage/InvokeReference.cls

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ Property Class As %String(MAXLEN = 255, XMLPROJECTION = "ATTRIBUTE") [ Required
99

1010
Property Method As %String(MAXLEN = 255, XMLPROJECTION = "ATTRIBUTE") [ Required ];
1111

12-
Property Phase As %String(MAXLEN = 255, XMLPROJECTION = "ATTRIBUTE") [ InitialExpression = "Configure" ];
12+
Property Phase As %IPM.DataType.PhaseName(XMLPROJECTION = "ATTRIBUTE") [ InitialExpression = "Configure" ];
1313

1414
/// If provided, the Phase property will be ignored. This CustomPhase will be used and no corresponding lifecycle is required.
15-
Property CustomPhase As %String(MAXLEN = 255, XMLPROJECTION = "ATTRIBUTE");
15+
Property CustomPhase As %IPM.DataType.CustomPhaseName(XMLPROJECTION = "ATTRIBUTE");
1616

17-
Property When As %String(MAXLEN = 255, VALUELIST = ",Before,After", XMLPROJECTION = "ATTRIBUTE") [ InitialExpression = "After", SqlFieldName = _WHEN ];
17+
Property When As %IPM.DataType.PhaseWhen(XMLPROJECTION = "ATTRIBUTE") [ InitialExpression = "After", SqlFieldName = _WHEN ];
1818

1919
Property CheckStatus As %Boolean(XMLPROJECTION = "ATTRIBUTE") [ InitialExpression = 0 ];
2020

src/cls/IPM/Storage/Module.cls

+31-3
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,27 @@ Method GetCustomPhases(Output pPhases)
177177
Set key = ""
178178
For {
179179
Set tInvoke = ..Invokes.GetNext(.key)
180-
Quit:(key = "")
180+
If key = "" {
181+
Quit
182+
}
181183
If (tInvoke.CustomPhase '= "") {
182184
Set pPhases($$$lcase(tInvoke.CustomPhase)) = tInvoke.CustomPhase
183185
}
184186
}
187+
For {
188+
Set tResource = ..Resources.GetNext(.key)
189+
If key = "" {
190+
Quit
191+
}
192+
Set tProcessor = tResource.Processor
193+
// %IsA() only returns true if the class is the "primary" superclass, while %Extends() works for other superclasses (such as mixins).
194+
If $IsObject(tProcessor) && tProcessor.%Extends("%IPM.ResourceProcessor.CustomPhaseMixin") {
195+
Set cp = tProcessor.CustomPhase
196+
If cp '= "" {
197+
Set pPhases($$$lcase(cp)) = cp
198+
}
199+
}
200+
}
185201
}
186202

187203
/// Execute multiple lifecycle phases in sequence. Execution is terminated if one fails.
@@ -350,7 +366,20 @@ ClassMethod ExecutePhases(pModuleName As %String, pPhases As %List, pIsComplete
350366
}
351367
Quit:$$$ISERR(tSC)
352368

353-
If 'tIsCustomPhase {
369+
If tIsCustomPhase {
370+
Set tKey = ""
371+
For {
372+
Set tResource = tModule.Resources.GetNext(.tKey)
373+
If tKey = "" {
374+
Quit
375+
}
376+
Set tProcessor = tResource.Processor
377+
// %IsA() only returns true if the class is the "primary" superclass, while %Extends() works for other superclasses (such as mixins).
378+
If $IsObject(tProcessor) && tProcessor.%Extends("%IPM.ResourceProcessor.CustomPhaseMixin") {
379+
$$$ThrowOnError(tProcessor.OnCustomPhase(tOnePhase, .pParams))
380+
}
381+
}
382+
} Else {
354383
// Lifecycle before / (phase) / after
355384
$$$ThrowOnError(tLifecycle.OnBeforePhase(tOnePhase,.pParams))
356385
$$$ThrowOnError($Method(tLifecycle,"%"_tOnePhase,.pParams))
@@ -1612,4 +1641,3 @@ Storage Default
16121641
}
16131642

16141643
}
1615-

0 commit comments

Comments
 (0)