An experiment in bringing dependency management, versioning, and modular
code distribution to OpenEdge ABL. See docs/research/ for the
comparative analysis (npm, pip, Maven/Ivy, existing OpenEdge tooling)
this design is based on.
This repo is just the plugin. The demo/consumer app and the registries/ packages it depends on live in their own separate repos - see "Remote registries" below.
No clone needed - pope is resolved by version from its published Maven repo (https://balticamadeus.github.io/pope/), the same way any other Gradle plugin is. That's just the maven-repo branch of this repo served via GitHub Pages, not a hosted registry service - chosen so publishing needs no new account or token, only a git branch pushed to on release.
- Wire up your ABL project - download
pope-init(pope-init.baton Windows) once, from github.com/BalticAmadeus/pope, and then you can run it from your every project's own directory that you want to wire up:It writes the Gradle wrapper,path-to-file/pope-init [<version>] # path-to-file\pope-init.bat on Windowssettings.gradle.kts, andbuild.gradle.kts(pointed at the published plugin), then lets Gradle finish the rest: generating/patchingopenedge-project.json, prompting for registries, and offering to install the global CLI (step 2). Safe to re-run. SeeHANDOVER.mdanddocs/spec/kotlin-gradle-files.mdfor what it actually does. - (Optional) Install the global CLI, so
pope install/uninstall/propath/prune/registry addwork from any project without a.//.\prefix.pope-initoffers this already - said no, or want it later? Run it directly:One-time, idempotent; open a new terminal after. This is the global~/.pope/cli/install.sh # %USERPROFILE%.pope\cli\install.ps1 on Windowscli/pope, not the per-projectpope/pope.bat: it finds its target project by walking up from your current directory (safe to put on PATH), while the per-project copy is tied to its own file location instead (zero-setup, e.g. in CI).
pope-init wire up a new/existing project (interactive)
pope version print the installed pope plugin version
pope install resolve declared dependencies
pope install <package>[:<versionSpec>] add + resolve a dependency in one step
pope uninstall <package> remove a dependency and clean up its files
pope propath [--tests] print the generated PROPATH
pope registry add [<prefix> <url>] add a registry (interactive if omitted)
pope prune [--dry-run] remove pope_packages/ entries no longer declared
pope prepare get this package ready to publish
A registry is just a plain git repo, no server or tooling involved - one
reference file per package version, at
packages/<name>/<version>.json. The filename is the version - the
file's content just says where to fetch it from:
{
"repoUrl": "https://github.com/yourorg/calculator.git",
"ref": "v1.0.0"
}ref is passed straight to git, so it can be anything git understands -
a tag (as above), a branch name, or a raw commit SHA - it doesn't need
to match the version number at all. Create the repo, add that file,
commit, push. That's the whole registry.
Publishing a new version means adding another <version>.json file, not
replacing the old one (see "Multi-version registries" above). Then point
a consumer project at it - see below.
A package is its own git repo, tagged at each version:
your-package/
openedge-project.json
src/
yourorg/
yourpackage/
YourClass.cls (class yourorg.yourpackage.YourClass:)
{
"name": "your-package",
"version": "1.0.0",
"popePackageName": "yourorg.yourpackage",
"popeDependencies": {},
"buildPath": [{ "type": "source", "path": "src" }]
}Run pope-init (or pope-init.bat on Windows) inside the package's own
directory to generate/patch openedge-project.json, which looks like this:
{
"name": "your-package",
"version": "1.0.0",
"popePackageName": "yourorg.yourpackage",
"popeDependencies": {},
"buildPath": [{ "type": "source", "path": "src" }],
"popeToolVersion": "1.2.0"
}Fold your org into
popePackageName/the namespace (e.g. yourorg.yourpackage, not bare
yourpackage) so it doesn't collide with someone else's package of the
same name - pope only catches an actual collision at resolve time (see
"PROPATH namespace-collision detection" above).
Before tagging, run pope prepare (global CLI) or ./gradlew popePrepare from the package's own checkout - it pins any
registry-style popeDependencies entries to their real {repoUrl, ref}
(a caret-range dependency only resolves for a consumer who happens to
have that same registry configured), refreshes popeToolVersion,
gitignores dev-only files, and fails loudly if popePackageName
disagrees with the real namespace in .cls files. It never touches git
or "version" - tag (git tag v1.0.0) and push yourself.
A registry maps a prefix to a catalog repo, so a dependency like
"Paulius.Util": "^0.0.1" resolves without a full {repoUrl, ref}.
Add one with pope registry add [<prefix> <url> [<name>]] (interactive
if omitted) - writes to pope-registries.properties at the project
root (committed, mergeable with any registries declared in
build.gradle.kts instead; a prefix declared in both is a
duplicate-prefix error). Then install by prefixed name
(pope install Paulius/Util) or let pope search every registry
(pope install Util). See
Registry for a real catalog
registry, and Util for what a
resolvable package looks like.
The core loop works end to end, against real, remote, git-hosted registries:
- Multi-registry, prefix-routed resolution - any number of registries,
each with its own routing prefix (e.g.
ba.,cw.). - Catalog-based registries - a registry is a small git repo holding
only reference files, no package content. Fetches through a
bare-clone-plus-
git worktreecache, so repeat fetches are local. Every package from a registry shares onepope_packages/<registryName>/folder; direct-source dependencies sharepope_packages/dependencies/. - Multi-version registries - a package's catalog folder can hold any number of version files; install picks the highest one satisfying the caret range.
- Direct-source dependencies - a package can depend on another by
inline
{repoUrl, ref}, no registry involved. - Transitive resolution with version-conflict detection across the whole graph, and PROPATH namespace-collision detection (two packages sharing a real ABL namespace fail loudly instead of silently shadowing each other).
- Integrity verification -
pope.lockrecords a content hash per package; a tag force-moved to different content fails loudly. - Tool-version compatibility check - every manifest pope writes gets
stamped with
popeToolVersion;pope installwarns (doesn't fail) if a resolved dependency's major version differs from the one currently running.pope versionprints your own installed version; thepopeStampGradle task (gradlew popeStamp, nopopeCLI shortcut) refreshes the stamp on a package with no Gradle wiring of its own, e.g. a leaf dependency - point a separate project'spope.projectRootat it and run the task from there. buildPathtest entries -type: "test"is excluded from PROPATH by default, included withpope propath --tests. Never leaks from a dependency into a consumer.pope prune [--dry-run]- removespope_packages//buildPathentries no longer part of the resolved graph.pope uninstall <package>- removes a dependency and cleans up itspope_packages//pope.lock/buildPathentries in one step. A bare local name works too if it matches exactly one declared dependency, and a typo gets a "did you mean X?" prompt against your declared dependencies.- Bare-name install -
pope install <name>with no registry given searches every configured registry; auto-installs on one match, prompts to choose on several. Typos get a "did you mean X?" prompt too. - Backward compatible: no
registries {}configured falls back to a plain local-directory registry.
See docs/decisions/ for what's been decided and why. Still missing:
include-collision linting, and real Gradle/Ivy-based resolution - version
matching (pope/version/) and graph resolution (pope/resolver/) are
hand-written instead, a known deviation from ADR-0001 worth raising with
the team before treating as settled.