Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
*.mac text eol=lf
*.int text eol=lf
Dockerfil* text eol=lf
ipm text eol=lf
iriscli text eol=lf
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down
43 changes: 43 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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
# List installed packages
docker exec -it <container-name, e.g. ipm-iris-1> ipm list

# Install a package
docker exec -it <container-name, e.g. ipm-iris-1> ipm install zpm-registry

# Run tests for a module
docker exec -it <container-name, e.g. ipm-iris-1> 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 <container-name, e.g. ipm-iris-1> 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 <namespace>` as the first argument. For example: `docker exec -it <container-name> 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:
Expand Down
73 changes: 73 additions & 0 deletions ipm
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/bin/bash

# ipm: convenience wrapper to run an IPM command in an IRIS session
#
# Usage: ipm [-U <namespace>] [-i <instance>] <IPM command and args...>
#
# Options:
# -U <namespace> IRIS namespace to run in (default: USER, or $IRIS_NAMESPACE / $IRISNAMESPACE)
# -i <instance> 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=()

# 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

# Parse command-line arguments
while [[ $# -gt 0 ]]; do
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 <namespace>] [-i <instance>] <IPM command and args...>" >&2
echo " ipm --help for more information" >&2
exit 1
fi

# Join the CMD array into a single string for the ObjectScript zpm call
CMDSTR="${CMD[*]}"
# Escape any double-quotes in the command string ("" is a literal " in ObjectScript strings)
CMDSTR_ESC=${CMDSTR//\"/\"\"}

# 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 "$INSTANCE" "${ARGS[@]}"
85 changes: 85 additions & 0 deletions iriscli
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/bin/bash

# iriscli: convenience wrapper to open an IRIS terminal session or run a script file
#
# Usage: iriscli [-U <namespace>] [-i <instance>] [<script file> [param...]]
#
# Options:
# -U <namespace> IRIS namespace to connect to (default: $IRIS_NAMESPACE / $IRISNAMESPACE)
# -i <instance> IRIS instance name (default: $ISC_PACKAGE_INSTANCENAME, then "iris")
# -h, --help Show this help message
#
# Examples:
# iriscli # interactive session
# iriscli -U USER # interactive session in USER namespace
# iriscli -i iris # interactive session on a named instance
# iriscli /path/to/script.script # run a script file and halt
# iriscli /path/to/script.script arg1 arg2 # run with params exposed as params(1), params(2)
#
# Script file format: ObjectScript, one command per line; blank lines and lines
# starting with ;, #, or // are ignored.

# Arrays to accumulate iris session flags and script parameters
ARGS=()
Comment thread
isc-kiyer marked this conversation as resolved.
PARAMS=()

# Default instance: prefer the container-standard env var, fall back to "iris"
INSTANCE="${ISC_PACKAGE_INSTANCENAME:-iris}"

# Path to the script file to run (empty = interactive mode)
file=

# 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

# Parse command-line arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
# Print the comment block at the top of this file, stripping the leading "# "
sed -n '3,20p' "$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
;;
*)
# First non-flag argument that is an existing file becomes the script to run;
# everything after that is treated as a parameter passed into the script.
if [ -z "$file" ] && [ -f "$1" ]; then
file=$1
else
PARAMS+=("$1")
fi
shift
;;
esac
done

if [ -n "$file" ]; then
# Script mode: pipe ObjectScript into iris session and halt when done.
# The subshell groups all input so iris session receives it as a single stdin stream.
(
# Expose any extra arguments as params(1), params(2), ... in ObjectScript.
# Double-quotes inside param values are escaped by doubling them ("" is a literal " in ObjectScript strings).
for param in "${PARAMS[@]}"; do
echo "Set params(\$i(params)) = \"${param//\"/\"\"}\""
done
# Strip comment lines (starting with ;, #, //) and blank lines before feeding to IRIS
grep -Ev '^(;|#|//)' "$file" | grep -v '^$'
echo halt
) | iris session "$INSTANCE" "${ARGS[@]}"
else
# Interactive mode: just open a session
iris session "$INSTANCE" "${ARGS[@]}"
fi
3 changes: 3 additions & 0 deletions module.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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. -->
<FileCopy Name="modules/python/rpds.py" Target="${mgrdir}python/rpds.py" />
<FileCopy Name="ipm" Target="${ipmdir}bin/ipm" />
<FileCopy Name="iriscli" Target="${ipmdir}bin/iriscli" />
<Invoke Class="IPM.Installer" Method="InstallScripts" Phase="Activate" When="After" />
</Module>
</Document>
</Export>
68 changes: 68 additions & 0 deletions preload/cls/IPM/Installer.cls
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Parameter VERSION;
/// Number of files for autoinstall release
Parameter FILESCOUNT = 0;

/// IPM's own module name. Mirrors $$$IPMModuleName in %IPM.Common — duplicated here
/// because this class is loaded during bootstrapping before that include is available.
Parameter MODULENAME = "zpm";

/// Application Definition
XData PM [ XMLNamespace = INSTALLER ]
{
Expand Down Expand Up @@ -476,4 +480,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 <FileCopy> in module.xml.
set module = ##class(%IPM.Storage.Module).NameOpen(..#MODULENAME, , .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
}

}
Loading