Cross-compile, assemble, link, and package MVS 3.8j projects from a modern host.
mbt is a reusable Git submodule that centralizes the full build pipeline for mvslovers projects targeting MVS 3.8j on Hercules.
As of v2.0.0 the default build compiles, assembles, links and packages entirely on the host with the cc370 toolchain (
cc370/as370/ld370/ar370); MVS is only touched bymake deploy. A project's Makefile is two lines:MBT_ROOT := mbt include $(MBT_ROOT)/mk/mbt.mkSee docs/MIGRATION.md for the new
project.tomlreference, the v1→v2 mapping, and migration steps.The legacy v1 (remote mvsMF/JCL) build documented below still works via
include $(MBT_ROOT)/mk/legacy/core.mkand lives undermk/legacy/+scripts/legacy/. It is in maintenance mode.
C source (.c)
└─► c2asm370 cross-compile to S/370 assembler (on host)
└─► IFOX00 assemble on MVS via mvsMF REST API
└─► IEWL (NCAL) link to NCALIB
└─► IEWL (final) build load module
└─► IEBCOPY install to target dataset
└─► TRANSMIT package as XMIT for release
Key properties:
- Single
project.tomlper project — no Makefile boilerplate - 2-line Makefile integrates the full pipeline via
make - Zero external Python dependencies — stdlib only
- All MVS communication via mvsMF REST API
- GitHub Releases as package registry — versioned deps with lockfile
- Works with any reachable MVS 3.8j system (local Hercules, remote TK4-, MVS/CE)
| Tool | Version | Purpose |
|---|---|---|
| Python | 3.12+ | Build scripts (stdlib only) |
| GNU Make | any | Orchestration |
| c2asm370 | any | C to S/370 cross-compiler |
| mvsMF | any | MVS REST API (running on target system) |
| NJE38 | 3.0.0+ | TRANSMIT / RECEIVE support on MVS target |
cd your-project
git submodule add https://github.com/mvslovers/mbt.git mbtMBT_ROOT := mbt
include $(MBT_ROOT)/mk/core.mkThat's it. All build targets are now available.
Create ~/.mbt/config.toml:
[mvs]
host = "myhost.lan"
port = 1080
user = "IBMUSER"
pass = "SYS1"
hlq = "IBMUSER"
deps_hlq = "IBMUSER.DEPS"
deps_volume = "WORK00"
[jes]
jobclass = "A"
msgclass = "H"SYS1.MACLIB and SYS1.AMODGEN are always included in the SYSLIB
concatenation — no configuration required. If your project needs
additional system macro libraries (e.g. SYS2.MACLIB), declare them
in project.toml:
[system]
maclibs = ["SYS2.MACLIB"][project]
name = "myapp"
version = "1.0.0"
type = "application"
[mvs.build.datasets.source]
suffix = "SOURCE"
dsorg = "PO"
recfm = "FB"
lrecl = 80
blksize = 3120
space = ["TRK", 5, 2, 5]
[mvs.build.datasets.punch]
suffix = "OBJECT"
dsorg = "PO"
recfm = "FB"
lrecl = 80
blksize = 3120
space = ["TRK", 5, 2, 5]
[mvs.build.datasets.ncalib]
suffix = "NCALIB"
dsorg = "PO"
recfm = "U"
lrecl = 0
blksize = 32760
space = ["TRK", 5, 2, 5]
[mvs.build.datasets.syslmod]
suffix = "LOAD"
dsorg = "PO"
recfm = "U"
lrecl = 0
blksize = 32760
space = ["TRK", 5, 2, 5]
[dependencies]
"mvslovers/crent370" = ">=1.0.0"
[link.module]
name = "MYAPP"
options = ["LIST", "XREF", "LET"]
[artifacts]
mvs = truemake doctor # verify environment
make bootstrap # resolve deps, provision datasets on MVS
make build # cross-compile C + assemble on MVS (incremental)
make link # final linkedit
make package # create release artifacts in dist/make build uploads source files to a SOURCE PDS on MVS and submits
multi-step batch JCL. It is incremental by default — only modules whose
source files changed since the last successful build are recompiled and
assembled. Use --force to rebuild everything, or --member NAME to
build a single module during development.
The project version from project.toml is automatically passed to
the cross-compiler as -DVERSION="x.y.z".
| Target | Description |
|---|---|
make doctor |
Check environment (Python, c2asm370, MVS connectivity) |
make bootstrap |
Resolve dependencies, upload to MVS, allocate datasets |
make update-deps |
Re-resolve all dependencies (ignore lockfile) and bootstrap |
make build |
Cross-compile and assemble (incremental — only changed modules) |
make build ARGS="--member HELLO" |
Build a single module |
make build ARGS="--force" |
Rebuild all modules (ignore stamps) |
make link |
Final linkedit to produce load module |
make install |
Copy build datasets to install datasets (IEBCOPY) |
make package |
Create release artifacts in dist/ |
make release VERSION=1.2.0 |
Bump version, tag, push, then bootstrap new datasets |
make graph |
Display dependency tree |
make datasets |
List all project datasets with status |
make clean |
Remove local build artifacts |
make distclean |
Full cleanup (contrib/, .mbt/, asm/*.s) |
Configuration is resolved in priority order:
- Environment variables (
MBT_MVS_HOST,MBT_MVS_PORT, etc.) - Project-local
.envfile - Global
~/.mbt/config.toml - Built-in defaults
| Variable | Default | Description |
|---|---|---|
MBT_MVS_HOST |
localhost |
MVS hostname or IP |
MBT_MVS_PORT |
1080 |
mvsMF API port |
MBT_MVS_USER |
IBMUSER |
MVS userid |
MBT_MVS_PASS |
MVS password | |
MBT_MVS_HLQ |
IBMUSER |
High-level qualifier for datasets |
MBT_MVS_DEPS_HLQ |
{HLQ}.DEPS |
HLQ for dependency datasets |
MBT_MVS_DEPS_VOLUME |
Volume for RECEIVE — see note below | |
MBT_JES_JOBCLASS |
A |
JES job class |
MBT_JES_MSGCLASS |
H |
JES message class |
MBT_BUILD_ID |
CI build number (enables CI dataset naming) |
MVS/CE users:
MBT_MVS_DEPS_VOLUMEmust be set in your.envfile. MVS/CE ships with NJE38 2.3.0, whose RECEIVE cannot locate a public volume on its own. Whether this is a bug in NJE38 2.3.0 or a MVS/CE sysgen issue is still under investigation. Until resolved, set a volume explicitly:# .env MBT_MVS_DEPS_VOLUME=PUB000This workaround is not required on TK4- or TK5.
| Type | Description | NCAL Link | Final Link | Artifacts |
|---|---|---|---|---|
runtime |
C runtime library (e.g. crent370) | yes | no | ncalib, maclib |
library |
Reusable library (e.g. ufs370) | yes | no | ncalib, maclib |
module |
Loadable module | yes | yes | syslmod |
application |
Standalone program (e.g. httpd) | yes | yes | syslmod |
Dependencies are declared in [dependencies] with semver constraints:
[dependencies]
"mvslovers/crent370" = ">=1.0.0"
"mvslovers/ufs370" = ">=0.9.0,<2.0.0"On make bootstrap, mbt:
- Resolves the best matching version from GitHub Releases
- Downloads release XMIT archives to
~/.mbt/cache/ - Uploads and RECEIVEs them on MVS (
{DEPS_HLQ}.{DEP}.{VRM}.*) - Extracts headers to
contrib/{dep}-{version}/include/ - Writes a lockfile to
.mbt/mvs.lock
The lockfile pins exact versions for reproducible builds. Commit it.
Use make bootstrap ARGS=--update to re-resolve all dependencies.
$ make graph
httpd v3.3.1
├─ crent370 v1.0.0
├─ ufs370 v1.0.0
│ └─ crent370 v1.0.0
└─ mqtt370 v1.0.0
├─ crent370 v1.0.0
└─ lua370 v1.0.0| Category | Pattern | Example |
|---|---|---|
| Build | {HLQ}.{PROJECT}.{VRM}.{SUFFIX} |
IBMUSER.HTTPD.V3R3M1.NCALIB |
| CI Build | {HLQ}.{PROJECT}.B{ID}.{SUFFIX} |
IBMUSER.HTTPD.B42.NCALIB |
| Dependency | {DEPS_HLQ}.{DEP}.{VRM}.{SUFFIX} |
IBMUSER.DEPS.CRENT370.V1R0M0.MACLIB |
| Install (fixed) | {HLQ}.{name} |
IBMUSER.HTTPD.LOAD |
make package creates release artifacts in dist/:
| File | Contents |
|---|---|
{name}-{ver}-load.xmit |
All load modules packed into one LINKLIB XMIT (projects with modules) |
{name}-{ver}-lib.tar.gz |
Static library archive + public headers (library projects only) |
The load XMIT is a single TSO-RECEIVE-ready file: every module's per-member
IEBCOPY unload (build/NAME.iebcopy, which carries the PDS2 directory) is
combined via ld370 --pack … -xmit into one LINKLIB. Its embedded restore
name is {NAME}.{VRM}.LINKLIB (e.g. FTPD.V1R0M0.LINKLIB); override it at
RECEIVE time with DATASET('your.own.linklib').
The -lib.tar.gz is what make deps consumes for downstream projects
(headers + .a under {name}-{ver}/).
mbt provides reusable GitHub Actions workflows:
# .github/workflows/build.yml
on: [push, pull_request]
jobs:
build:
uses: mvslovers/mbt/.github/workflows/build.yml@main
secrets:
MVS_HOST: ${{ secrets.MVS_HOST }}
MVS_PORT: ${{ secrets.MVS_PORT }}
MVS_USER: ${{ secrets.MVS_USER }}
MVS_PASS: ${{ secrets.MVS_PASS }}# .github/workflows/release.yml
on:
push:
tags: ["v*"]
jobs:
release:
uses: mvslovers/mbt/.github/workflows/release.yml@main
secrets:
MVS_HOST: ${{ secrets.MVS_HOST }}
MVS_PORT: ${{ secrets.MVS_PORT }}
MVS_USER: ${{ secrets.MVS_USER }}
MVS_PASS: ${{ secrets.MVS_PASS }}The release workflow validates that the tag matches project.toml version,
runs the full pipeline, and creates a GitHub Release with dist/* as assets.
make release VERSION=1.2.0This updates the version in all files listed in [release] version_files,
commits, tags v1.2.0, and pushes. CI then builds and publishes the release.
your-project/
├── project.toml # project definition
├── Makefile # 2 lines: MBT_ROOT + include
├── .env # local MVS connection overrides (gitignored)
├── .mbt/
│ ├── mvs.lock # pinned dependency versions (committed)
│ ├── stamps/ # SHA256 stamps for incremental builds
│ └── logs/ # JES spool logs on failure
├── src/ # C sources
├── asm/ # assembler sources + generated .s files
├── include/ # public headers (libraries)
├── contrib/ # extracted dependency headers (gitignored)
├── dist/ # release artifacts (gitignored)
└── mbt/ # this repo (git submodule)
examples/hello370 — a minimal C + assembler application that depends on crent370. Demonstrates the full pipeline.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Build failure (RC > max_rc, link error) |
| 2 | Configuration error |
| 3 | Dependency error |
| 4 | Mainframe communication error |
| 5 | Dataset error |
| 99 | Internal error |
MIT