diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f72ed274..64e8ba38b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #1001: The `unmap` and `enable` commands will now only activate CPF merge once after all namespaces have been configured instead after every namespace - #1052: In a namespace with mapped IPM, the `info` command works again and the intro message displays the IPM version and where its mapped from - #1102: %IPM.Storage.QualifiedModuleInfo:%New() will now copy over version properties when passed in a resolvedReference +- #1112: Packaging a module with a globals resource now respects SourcesRoot, placing the exported file at the correct path in the tarball ## [0.10.6] - 2026-02-24 diff --git a/src/cls/IPM/ResourceProcessor/Default/Global.cls b/src/cls/IPM/ResourceProcessor/Default/Global.cls index 16829f29a..fb57d01f3 100644 --- a/src/cls/IPM/ResourceProcessor/Default/Global.cls +++ b/src/cls/IPM/ResourceProcessor/Default/Global.cls @@ -5,7 +5,7 @@ Class %IPM.ResourceProcessor.Default.Global Extends %IPM.ResourceProcessor.Abstr Parameter DESCRIPTION As STRING = "Standard resource processor for global exports."; /// Comma-separated list of resource attribute names that this processor uses -Parameter ATTRIBUTES As STRING = "Global,Preserve"; +Parameter ATTRIBUTES As STRING = "Global,Preserve,Directory,FilenameTranslateIdentifier,FilenameTranslateAssociator"; /// Optional name of global within the file Property Global As %IPM.DataType.GlobalReference; @@ -17,6 +17,12 @@ Property Preserve As %Boolean [ InitialExpression = 0 ]; /// Defaults to the resource's extension (lower-case) if unspecified. Property Directory As %String(MAXLEN = "") [ InitialExpression = "gbl/" ]; +/// Characters in the resource name to translate when building the filename. +Property FilenameTranslateIdentifier As %String [ InitialExpression = "%,("")" ]; + +/// Replacement characters corresponding to FilenameTranslateIdentifier. +Property FilenameTranslateAssociator As %String [ InitialExpression = "___" ]; + Method OnPhase( pPhase As %String, ByRef pParams, @@ -51,30 +57,16 @@ Method OnPhase( } if '..ResourceReference.Generated { - set tSubDirectory = $select(..ResourceReference.Preload:"preload/",1:"") - set tResourceDirectory = tRoot _ "/" _ tSubDirectory - - set tSourceRoot = ..ResourceReference.Module.SourcesRoot - if tSourceRoot'="","\/"'[$extract(tSourceRoot, *) { - set tSourceRoot = tSourceRoot _ "/" - } - - set tDirectory = ..Directory - if tDirectory'="","\/"'[$extract(tDirectory, *) { - set tDirectory = tDirectory _ "/" - } else { - set tDirectory = "gbl/" - } - - set tResourceDirectory = ##class(%File).NormalizeDirectory(tResourceDirectory_tSourceRoot_tDirectory) + set relativePath = ..OnItemRelativePath(..ResourceReference.Name) + set resourcePath = ##class(%File).NormalizeFilename(tRoot _ "/" _ relativePath) + set resourceDirectory = ##class(%File).GetDirectory(resourcePath) if tDeveloperMode { - set ^Sources("GBL",tName) = tSourcesPrefix_tResourceDirectory + set ^Sources("GBL", tName) = tSourcesPrefix _ resourceDirectory } if '..ResourceReference.Preload { - set tResourcePath = tResourceDirectory_$translate(tName,"%,("")","___")_".xml" - set tSC = $system.OBJ.Load(tResourcePath,$select(tVerbose:"/display",1:"/nodisplay")_"/nocompile") + set tSC = $system.OBJ.Load(resourcePath, $select(tVerbose:"/display", 1:"/nodisplay") _ "/nocompile") if $$$ISERR(tSC) { quit } @@ -93,4 +85,33 @@ Method OnPhase( quit tSC } +/// Sets the relative path for the globals resource so the packaging phase +/// exports the file to the correct location, respecting SourcesRoot. +Method OnResolveChildren(ByRef resourceArray) As %Status +{ + set resourceArray(..ResourceReference.Name, "RelativePath") = ..OnItemRelativePath(..ResourceReference.Name) + quit $$$OK +} + +/// Returns the path of itemName relative to the module root, +/// including SourcesRoot and the configured Directory. +Method OnItemRelativePath(itemName As %String) As %String +{ + set sourceRoot = ..ResourceReference.Module.SourcesRoot + // Append trailing slash only if sourceRoot is non-empty and doesn't already end with / or \ + if (sourceRoot '= "") && ("\/" '[ $extract(sourceRoot, *)) { + set sourceRoot = sourceRoot _ "/" + } + + set directory = ..Directory + if directory = "" { + set directory = "gbl/" + } elseif "\/" '[ $extract(directory, *) { + set directory = directory _ "/" + } + + set name = $piece(itemName, ".", 1, * - 1) + quit $select(..ResourceReference.Preload:"preload/", 1:"") _ sourceRoot _ directory _ $translate(name, ..FilenameTranslateIdentifier, ..FilenameTranslateAssociator) _ ".xml" +} + } diff --git a/tests/integration_tests/Test/PM/Integration/GlobalsPackaging.cls b/tests/integration_tests/Test/PM/Integration/GlobalsPackaging.cls new file mode 100644 index 000000000..dcc584be0 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/GlobalsPackaging.cls @@ -0,0 +1,40 @@ +Class Test.PM.Integration.GlobalsPackaging Extends Test.PM.Integration.Base +{ + +/// Verifies that packaging a module with a globals resource and SourcesRoot +/// exports the file to the correct path and produces a loadable tarball. +Method TestGlobalsPackageWithSourcesRoot() +{ + set sc = $$$OK + try { + set moduleDir = ..GetModuleDir("globals") + + // Ensure clean state before loading from source + do ##class(%IPM.Main).Shell("uninstall globals-test") + + set sc = ##class(%IPM.Main).Shell("load " _ moduleDir) + do $$$AssertStatusOK(sc, "Loaded globals-test module successfully.") + + set tempDir = ##class(%Library.File).TempFilename() _ "dir" + set sc = ##class(%IPM.Main).Shell("globals-test package -DPath=" _ tempDir) + do $$$AssertStatusOK(sc, "Packaged globals-test module successfully.") + + set outDir = ##class(%Library.File).NormalizeDirectory(##class(%Library.File).TempFilename() _ "dir-out") + set sc = ##class(%IPM.General.Archive).Extract(tempDir _ ".tgz", outDir, .extractOutput) + do $$$AssertStatusOK(sc, "Extracted tarball.") + + do $$$AssertTrue(##class(%File).Exists(outDir _ "src/gbl/My.Settings.xml"), "Global exported to src/gbl/My.Settings.xml (SourcesRoot respected).") + + // Uninstall before loading from tarball to get a clean load assertion + do ##class(%IPM.Main).Shell("uninstall globals-test") + + set sc = ##class(%IPM.Main).Shell("load " _ tempDir _ ".tgz") + do $$$AssertStatusOK(sc, "Loaded globals-test from tarball successfully.") + + do $$$AssertEquals($get(^My.Settings("Parameter")), 42, "^My.Settings imported correctly from tarball.") + } catch e { + do $$$AssertStatusOK(e.AsStatus(), "An exception occurred.") + } +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/_data/globals/module.xml b/tests/integration_tests/Test/PM/Integration/_data/globals/module.xml new file mode 100644 index 000000000..c016dbfa0 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/globals/module.xml @@ -0,0 +1,12 @@ + + + + + globals-test + 1.0.0 + module + src + + + + diff --git a/tests/integration_tests/Test/PM/Integration/_data/globals/src/gbl/My.Settings.xml b/tests/integration_tests/Test/PM/Integration/_data/globals/src/gbl/My.Settings.xml new file mode 100644 index 000000000..0701ca5ba --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/globals/src/gbl/My.Settings.xml @@ -0,0 +1,13 @@ + + + +^My.Settings + Mode + Example + + Parameter + 42 + + + +