From c7bb200ed258f0f62352218eb81470fb62ece783 Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Mon, 20 Apr 2026 11:15:42 -0400 Subject: [PATCH 1/4] Add iriscli and ipm container utility scripts --- CHANGELOG.md | 1 + CONTRIBUTING.md | 43 +++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 5 +++++ ipm | 44 ++++++++++++++++++++++++++++++++++++++++++++ iriscli | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+) create mode 100644 ipm create mode 100644 iriscli diff --git a/CHANGELOG.md b/CHANGELOG.md index b6470f007..7187bd1f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - #992: Implement automatic history purge logic - #973: Enables CORS and JWT configuration for WebApplications in module.xml +- #1110: Add `iriscli` and `ipm` container utility scripts ### Fixed - #1001: The `unmap` and `enable` commands will now only activate CPF merge once after all namespaces have been configured instead after every namespace diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae82f6b40..7f354c493 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,6 +92,49 @@ From time to time, you may also want to remove unused Docker data to save disk s docker system prune -a ``` +#### Convenience Scripts: `ipm` and `iriscli` + +The repo ships two bash scripts that wrap common IRIS interactions so you don't have to type `iris session iris` boilerplate every time. They are copied into `/home/irisowner/bin/` during the container build and are available by name in any running container. + +**`ipm`** — runs a single IPM/ZPM command non-interactively and exits: +```bash +# List installed packages +docker exec -it ipm list + +# Install a package +docker exec -it ipm install zpm-registry + +# Run tests for a module +docker exec -it ipm test mymodule -only + +# Using docker compose exec (works for any container defined in docker-compose.yml) +# Note: must be run from the directory containing docker-compose.yml +docker compose exec iris ipm list +``` + +**`iriscli`** — opens an interactive IRIS terminal session: +```bash +# Either this command +docker exec -it iriscli + +# Or this equivalent command (must be run from the directory containing docker-compose.yml) +docker compose exec -it iris iriscli +``` + +`iriscli` also accepts an ObjectScript script file, executing each line and then halting: +```bash +docker compose exec -it iris iriscli /path/to/demo.script +``` + +A sample `demo.script`: +```objectscript +zn "USER" +write $zversion,! +zpm "list" +``` + +> **Namespace:** Both scripts respect the `IRIS_NAMESPACE` environment variable, or accept `-U ` as the first argument. For example: `docker exec -it ipm -U %SYS list`. + ### Developing IPM in an Existing IRIS Instance If you already have an IRIS instance running and you want to test IPM in this instance, run the following command: diff --git a/Dockerfile b/Dockerfile index f78088422..7954c0d36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,11 @@ FROM ${BASE} ARG REGISTRY=https://pm.community.intersystems.com +# --- Create dirs and place tools (use --chown to avoid extra chown layers) --- +COPY --chown=irisowner:irisowner iriscli /home/irisowner/bin/iriscli +COPY --chown=irisowner:irisowner ipm /home/irisowner/bin/ipm +RUN chmod +x /home/irisowner/bin/iriscli /home/irisowner/bin/ipm + RUN --mount=type=bind,src=.,dst=/home/irisowner/zpm/ \ iris start iris && \ iris session IRIS < /home/irisowner/zpm/iris.script && \ diff --git a/ipm b/ipm new file mode 100644 index 000000000..c07ae7679 --- /dev/null +++ b/ipm @@ -0,0 +1,44 @@ +#!/bin/bash + +# ipm: convenience wrapper to run a ZPM command in an IRIS session +# Usage examples: +# ipm isc.genai test -only +# ipm -U PLAZA isc.genai test -only + +ARGS=() +CMD=() + +# Allow namespace via env like iriscli +if [ -n "$IRIS_NAMESPACE" ]; then + ARGS+=( -U "$IRIS_NAMESPACE" ) +elif [ -n "$IRISNAMESPACE" ]; then + ARGS+=( -U "$IRISNAMESPACE" ) +fi + +# Optional leading -U +if [ "$1" = "-U" ] && [ -n "$2" ]; then + ARGS+=( -U "$2" ) + shift 2 +fi + +# Remaining args compose the ZPM command string +while [[ $# -gt 0 ]]; do + CMD+=( "$1" ) + shift +done + +if [ ${#CMD[@]} -eq 0 ]; then + echo "Usage: ipm [ -U ] " >&2 + exit 1 +fi + +# Join CMD array into a single string +CMDSTR="${CMD[*]}" +# Escape quotes for ObjectScript string literal +CMDSTR_ESC=${CMDSTR//\"/\"\"} + +# Build an OS snippet and feed to iris session +( + echo "zpm \"${CMDSTR_ESC}\":1:1" + echo "halt" +) | iris session ${ISC_PACKAGE_INSTANCENAME:-iris} "${ARGS[@]}" diff --git a/iriscli b/iriscli new file mode 100644 index 000000000..11a8664ee --- /dev/null +++ b/iriscli @@ -0,0 +1,34 @@ +#!/bin/bash + +ARGS=() +PARAMS=() +file= + +if [ -n "$IRIS_NAMESPACE" ]; then + ARGS+=( -U $IRIS_NAMESPACE ) +elif [ -n "$IRISNAMESPACE" ]; then + ARGS+=( -U $IRISNAMESPACE ) +fi + +while [[ $# -gt 0 ]]; do + if [ -x $1 ]; then + file=$1 + elif [ -z "$file" ]; then + ARGS+=("$1") + else + PARAMS+=("$1") + fi + shift +done + +if [ -n "$file" ]; then + ( + for param in ${PARAMS[@]}; do + echo "Set params(\$i(params)) = \"${param//\"/\"\"}\"" + done + egrep -v '^(;|#|//)|^$' $file; + echo halt + ) | iris session $ISC_PACKAGE_INSTANCENAME "${ARGS[@]}" +else + iris session iris "${ARGS[@]}" +fi \ No newline at end of file From 1a7f2e9e3ce9449160685be40a8f5e9792a3881a Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Mon, 20 Apr 2026 15:40:17 -0400 Subject: [PATCH 2/4] Enable ipm and iriscli scripts to work outside of containers --- .gitattributes | 2 ++ CHANGELOG.md | 2 +- CONTRIBUTING.md | 2 +- Dockerfile | 5 --- module.xml | 3 ++ preload/cls/IPM/Installer.cls | 64 +++++++++++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 7 deletions(-) diff --git a/.gitattributes b/.gitattributes index 4a94c94c8..ee3ba6fec 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,3 +9,5 @@ *.mac text eol=lf *.int text eol=lf Dockerfil* text eol=lf +ipm text eol=lf +iriscli text eol=lf diff --git a/CHANGELOG.md b/CHANGELOG.md index 7187bd1f8..2a3609c8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - #992: Implement automatic history purge logic - #973: Enables CORS and JWT configuration for WebApplications in module.xml -- #1110: Add `iriscli` and `ipm` container utility scripts +- #1110: Add `iriscli` and `ipm` container utility scripts that are auto-installed to `~/.local/bin/` and `~/bin/` so they work both inside and outside of containers (Unix/Linux only) ### Fixed - #1001: The `unmap` and `enable` commands will now only activate CPF merge once after all namespaces have been configured instead after every namespace diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f354c493..1ed7872b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,7 +94,7 @@ docker system prune -a #### Convenience Scripts: `ipm` and `iriscli` -The repo ships two bash scripts that wrap common IRIS interactions so you don't have to type `iris session iris` boilerplate every time. They are copied into `/home/irisowner/bin/` during the container build and are available by name in any running container. +The repo ships two bash scripts that wrap common IRIS interactions so you don't have to type `iris session iris` boilerplate every time. When IPM is installed on Unix/Linux, they are automatically copied to `~/.local/bin/` and `~/bin/` (if it exists), making them available on PATH in most environments including the dev containers. **`ipm`** — runs a single IPM/ZPM command non-interactively and exits: ```bash diff --git a/Dockerfile b/Dockerfile index 7954c0d36..f78088422 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,11 +4,6 @@ FROM ${BASE} ARG REGISTRY=https://pm.community.intersystems.com -# --- Create dirs and place tools (use --chown to avoid extra chown layers) --- -COPY --chown=irisowner:irisowner iriscli /home/irisowner/bin/iriscli -COPY --chown=irisowner:irisowner ipm /home/irisowner/bin/ipm -RUN chmod +x /home/irisowner/bin/iriscli /home/irisowner/bin/ipm - RUN --mount=type=bind,src=.,dst=/home/irisowner/zpm/ \ iris start iris && \ iris session IRIS < /home/irisowner/zpm/iris.script && \ diff --git a/module.xml b/module.xml index 8bbc0db73..1e40da2b1 100644 --- a/module.xml +++ b/module.xml @@ -52,6 +52,9 @@ This intentionally and necessarily masks possible installation of the real rpds-py package for the sake of working in a container environment with durable %SYS. --> + + + diff --git a/preload/cls/IPM/Installer.cls b/preload/cls/IPM/Installer.cls index 26970544b..8a563ff94 100644 --- a/preload/cls/IPM/Installer.cls +++ b/preload/cls/IPM/Installer.cls @@ -476,4 +476,68 @@ ClassMethod CleanupOldMappings() As %Status return sc } +/// Copies the ipm and iriscli scripts to the user's local bin directories. +/// Runs after Activate so the module version is resolvable in storage. +/// Always returns $$$OK — script installation is best-effort and must not block the install. +ClassMethod InstallScripts() As %Status +{ + // Scripts are bash; skip entirely on non-Unix platforms. + if '$$$isUNIX { + quit $$$OK + } + try { + // The scripts are staged under ${ipmdir}bin/ by in module.xml. + set module = ##class(%IPM.Storage.Module).NameOpen("ZPM", , .openSC) + $$$ThrowOnError(openSC) + if '$isobject(module) { + write !,"Could not resolve module directory for script installation." + quit + } + set binDir = ##class(%Library.File).NormalizeDirectory($system.Util.DataDirectory() _ "ipm/" _ module.Name _ "/" _ module.VersionString _ "/bin") + + set homeDir = $system.Util.GetEnviron("HOME") + if homeDir = "" { + write !,"$HOME not set, skipping script installation." + quit + } + set localBin = ##class(%Library.File).NormalizeDirectory(homeDir _ "/.local/bin") + + // ~/.local/bin is the XDG standard for user scripts; create it if absent. + if '##class(%Library.File).DirectoryExists(localBin) { + if '##class(%Library.File).CreateDirectoryChain(localBin) { + $$$ThrowStatus($$$ERROR($$$GeneralError, "Could not create directory: " _ localBin)) + } + } + + // ~/bin is on PATH in the IRIS container; ~/.local/bin covers most Unix/Linux distros. + // We skip ~/bin if it doesn't exist to avoid creating unexpected directories on systems that don't use it. + set homeBin = ##class(%Library.File).NormalizeDirectory(homeDir _ "/bin") + + for script = "ipm","iriscli" { + set src = binDir _ script + if '##class(%Library.File).Exists(src) { + write !,"Warning - source script not found: ",src + continue + } + for targetDir = localBin,homeBin { + if (targetDir = homeBin) && '##class(%Library.File).DirectoryExists(homeBin) { + continue + } + set dest = targetDir _ script + if '##class(%Library.File).CopyFile(src, dest, 1) { + write !,"Warning - could not copy ",script," to ",targetDir + continue + } + set cmd = $listbuild("chmod", "+x", dest) + $$$ThrowOnError(##class(%IPM.Utils.Module).RunCommand(, cmd, , , .rc)) + } + } + write !,"Installed ipm and iriscli scripts to ",localBin + } catch ex { + // Swallow all errors — a failed script copy must not fail the module install. + write !,"Warning - script installation failed: ",ex.DisplayString() + } + quit $$$OK +} + } From 373a05e19b5fa192e3fdd01cf89926c2d6564881 Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Tue, 21 Apr 2026 10:06:09 -0400 Subject: [PATCH 3/4] Add documentation, -h help flag, and -i IRIS instance flag --- ipm | 67 +++++++++++++++++++++-------- iriscli | 81 ++++++++++++++++++++++++++++------- preload/cls/IPM/Installer.cls | 4 +- 3 files changed, 117 insertions(+), 35 deletions(-) diff --git a/ipm b/ipm index c07ae7679..402cec574 100644 --- a/ipm +++ b/ipm @@ -1,44 +1,73 @@ #!/bin/bash -# ipm: convenience wrapper to run a ZPM command in an IRIS session -# Usage examples: -# ipm isc.genai test -only -# ipm -U PLAZA isc.genai test -only +# ipm: convenience wrapper to run an IPM command in an IRIS session +# +# Usage: ipm [-U ] [-i ] +# +# Options: +# -U IRIS namespace to run in (default: USER, or $IRIS_NAMESPACE / $IRISNAMESPACE) +# -i IRIS instance name (default: $ISC_PACKAGE_INSTANCENAME, then "iris") +# -h, --help Show this help message +# +# Examples: +# ipm list # list all installed modules +# ipm install MyModule # install a module from the registry +# ipm test MyModule -only # run tests for a module +# ipm -U USER list # run in a specific namespace +# ipm -i iris list # run against a specific IRIS instance +# Arrays to accumulate iris session flags and the IPM command tokens ARGS=() CMD=() -# Allow namespace via env like iriscli +# Default instance: prefer the container-standard env var, fall back to "iris" +INSTANCE="${ISC_PACKAGE_INSTANCENAME:-iris}" + +# Seed ARGS with a namespace from the environment if one is set if [ -n "$IRIS_NAMESPACE" ]; then ARGS+=( -U "$IRIS_NAMESPACE" ) elif [ -n "$IRISNAMESPACE" ]; then ARGS+=( -U "$IRISNAMESPACE" ) fi -# Optional leading -U -if [ "$1" = "-U" ] && [ -n "$2" ]; then - ARGS+=( -U "$2" ) - shift 2 -fi - -# Remaining args compose the ZPM command string +# Parse command-line arguments while [[ $# -gt 0 ]]; do - CMD+=( "$1" ) - shift + case "$1" in + -h|--help) + # Print the comment block at the top of this file, stripping the leading "# " + sed -n '3,17p' "$0" | sed 's/^# \?//' + exit 0 + ;; + -U) + # Explicit -U overrides any namespace set from the environment + ARGS=( -U "$2" ) + shift 2 + ;; + -i) + INSTANCE="$2" + shift 2 + ;; + *) + # All remaining arguments form the IPM command string + CMD+=( "$1" ) + shift + ;; + esac done if [ ${#CMD[@]} -eq 0 ]; then - echo "Usage: ipm [ -U ] " >&2 + echo "Usage: ipm [-U ] [-i ] " >&2 + echo " ipm --help for more information" >&2 exit 1 fi -# Join CMD array into a single string +# Join the CMD array into a single string for the ObjectScript zpm call CMDSTR="${CMD[*]}" -# Escape quotes for ObjectScript string literal +# Escape any double-quotes in the command string ("" is a literal " in ObjectScript strings) CMDSTR_ESC=${CMDSTR//\"/\"\"} -# Build an OS snippet and feed to iris session +# Pipe the zpm command into iris session; :1:1 flags suppress the session banner and prompt ( echo "zpm \"${CMDSTR_ESC}\":1:1" echo "halt" -) | iris session ${ISC_PACKAGE_INSTANCENAME:-iris} "${ARGS[@]}" +) | iris session "$INSTANCE" "${ARGS[@]}" diff --git a/iriscli b/iriscli index 11a8664ee..b00172143 100644 --- a/iriscli +++ b/iriscli @@ -1,34 +1,85 @@ #!/bin/bash +# iriscli: convenience wrapper to open an IRIS terminal session or run a script file +# +# Usage: iriscli [-U ] [-i ] [