2025-10-19 12:48:47 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
import subprocess
|
|
|
|
|
import tempfile
|
|
|
|
|
import os
|
2026-02-06 16:50:30 +00:00
|
|
|
import time
|
2025-10-19 12:48:47 +00:00
|
|
|
|
2026-02-03 14:39:29 +00:00
|
|
|
# Force temp files into a folder inside your project (fully owned by you)
|
|
|
|
|
custom_temp_dir = os.path.join(os.getcwd(), "tmp")
|
|
|
|
|
os.makedirs(custom_temp_dir, exist_ok=True)
|
|
|
|
|
tempfile.tempdir = custom_temp_dir
|
2025-10-19 12:48:47 +00:00
|
|
|
|
2025-10-19 13:33:13 +00:00
|
|
|
SRCLIB = "src"
|
|
|
|
|
JCLLIB = "jcl"
|
2025-10-19 12:48:47 +00:00
|
|
|
MVSHOST = "oldcomputernerd.com"
|
|
|
|
|
RDRPORT = 3505
|
2026-02-03 11:15:18 +00:00
|
|
|
MVS_PASSWORD = os.environ.get("MVS_BATCH_PASSWORD")
|
2025-10-19 12:48:47 +00:00
|
|
|
|
|
|
|
|
|
2026-02-03 16:10:03 +00:00
|
|
|
def create_jcl_payload(local_file, dataset_name, member_name):
|
|
|
|
|
|
2025-10-19 13:33:13 +00:00
|
|
|
with open(local_file, 'r') as f:
|
2026-02-03 16:10:03 +00:00
|
|
|
sysin = f.readlines()
|
2025-10-19 12:48:47 +00:00
|
|
|
|
|
|
|
|
# PDS member: Use IEBUPDTE
|
|
|
|
|
jcl = f"""
|
|
|
|
|
//UPLOAD JOB (ACCT),'UPLOAD',
|
2026-02-03 11:15:18 +00:00
|
|
|
// USER=@05054,PASSWORD={MVS_PASSWORD},
|
2025-10-19 12:48:47 +00:00
|
|
|
// CLASS=A,MSGCLASS=H,NOTIFY=@05054
|
|
|
|
|
//COPY EXEC PGM=IEBUPDTE,PARM=NEW
|
|
|
|
|
//SYSPRINT DD SYSOUT=*
|
|
|
|
|
//SYSUT1 DD DUMMY
|
|
|
|
|
//SYSUT2 DD DSN={dataset_name},DISP=MOD,UNIT=SYSDA,
|
|
|
|
|
// DCB=(RECFM=FB,LRECL=80,BLKSIZE=0)
|
|
|
|
|
//SYSIN DD *
|
|
|
|
|
"""
|
|
|
|
|
# Append control statement, source lines, end, and terminator (no leading space on ./)
|
|
|
|
|
jcl += f"./ ADD NAME={member_name}\n"
|
2026-02-03 16:10:03 +00:00
|
|
|
for line in sysin:
|
2026-02-06 10:54:45 +00:00
|
|
|
line = line.rstrip('\n')
|
|
|
|
|
stripped = line.lstrip()
|
|
|
|
|
# Skip comment lines that would be interpreted as JCL
|
|
|
|
|
if stripped.startswith('//') or stripped.startswith('/*'):
|
|
|
|
|
continue
|
|
|
|
|
jcl += line[:80].ljust(80) + "\n"
|
2025-10-19 12:48:47 +00:00
|
|
|
jcl += "./ ENDUP\n"
|
|
|
|
|
jcl += "/*\n"
|
2026-02-03 14:39:29 +00:00
|
|
|
|
2026-02-03 16:10:03 +00:00
|
|
|
return jcl
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def upload_source(local_file, dataset_name, member_name, mvshost=MVSHOST):
|
|
|
|
|
"""Upload source code to MVS PDS member"""
|
|
|
|
|
|
|
|
|
|
# Read the source file
|
|
|
|
|
# full path will come from the job runner
|
|
|
|
|
# filepath = os.path.join(SRCLIB, local_file)
|
|
|
|
|
payload = create_jcl_payload(local_file, dataset_name, member_name)
|
|
|
|
|
|
2025-10-19 12:48:47 +00:00
|
|
|
# Write JCL to temporary file and submit via netcat
|
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.jcl') as tmpfile:
|
2026-02-03 16:10:03 +00:00
|
|
|
tmpfile.write(payload)
|
|
|
|
|
tmpfile.flush()
|
2025-10-19 12:48:47 +00:00
|
|
|
tmpfile_path = tmpfile.name
|
|
|
|
|
try:
|
2026-02-03 16:10:03 +00:00
|
|
|
with open(tmpfile_path, 'rb') as f:
|
2026-02-06 12:32:58 +00:00
|
|
|
result = subprocess.run(
|
2026-02-03 16:10:03 +00:00
|
|
|
['nc', '-w', '5', mvshost, str(RDRPORT)],
|
|
|
|
|
input=f.read(),
|
|
|
|
|
check=True,
|
|
|
|
|
capture_output=True
|
|
|
|
|
)
|
2025-10-19 12:48:47 +00:00
|
|
|
print(f"Uploaded {local_file} to {dataset_name}({member_name})")
|
2026-02-06 12:32:58 +00:00
|
|
|
if result.stdout:
|
|
|
|
|
print("JES response:", result.stdout.decode(errors='ignore').strip())
|
2025-10-19 12:48:47 +00:00
|
|
|
return 0
|
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
|
print(f"Upload failed: {e}")
|
2026-02-03 16:10:03 +00:00
|
|
|
print("stderr:", e.stderr.decode(errors='ignore'))
|
2025-10-19 12:48:47 +00:00
|
|
|
return 1
|
2026-02-03 16:10:03 +00:00
|
|
|
|
2025-10-19 12:48:47 +00:00
|
|
|
finally:
|
2026-02-03 16:10:03 +00:00
|
|
|
# Clean up outside
|
2026-02-04 14:42:23 +00:00
|
|
|
os.unlink(tmpfile_path)
|
2025-10-19 12:48:47 +00:00
|
|
|
|
|
|
|
|
def submit_jcl(job, mvshost="oldcomputernerd.com"):
|
|
|
|
|
"""Submit JCL job from local directory"""
|
2026-02-06 10:03:53 +00:00
|
|
|
subjcl = os.path.join(JCLLIB, f"{job.upper()}.jcl")
|
2025-10-19 12:48:47 +00:00
|
|
|
|
|
|
|
|
if not os.path.exists(subjcl):
|
|
|
|
|
print(f"JCL file {subjcl} not found")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
try:
|
2026-02-06 16:45:57 +00:00
|
|
|
# Read the JCL file and send via netcat (same approach as upload_source)
|
|
|
|
|
with open(subjcl, 'rb') as f:
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
['nc', '-w', '5', mvshost, str(RDRPORT)],
|
|
|
|
|
input=f.read(),
|
|
|
|
|
check=True,
|
|
|
|
|
capture_output=True
|
|
|
|
|
)
|
2025-10-19 12:48:47 +00:00
|
|
|
print(f"Submitted JCL job: {job}")
|
2026-02-06 12:32:58 +00:00
|
|
|
if result.stdout:
|
|
|
|
|
print("JES response:", result.stdout.decode(errors='ignore').strip())
|
2025-10-19 12:48:47 +00:00
|
|
|
return 0
|
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
|
print(f"JCL submission failed: {e}")
|
2026-02-06 16:45:57 +00:00
|
|
|
print("stderr:", e.stderr.decode(errors='ignore'))
|
|
|
|
|
return 1
|
|
|
|
|
except FileNotFoundError as e:
|
|
|
|
|
print(f"Error reading JCL file: {e}")
|
2025-10-19 12:48:47 +00:00
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2026-02-06 09:29:52 +00:00
|
|
|
if len(sys.argv) < 2:
|
|
|
|
|
print("Usage: mvs_job.py <local_source_file> [destination_pds] [mvshost]")
|
|
|
|
|
print()
|
|
|
|
|
print("Arguments:")
|
|
|
|
|
print(" local_source_file - Path to source file (required)")
|
|
|
|
|
print(" destination_pds - PDS destination as DATASET(MEMBER) (optional)")
|
|
|
|
|
print(" Default: @05054.C90.SOURCE(basename)")
|
|
|
|
|
print(" mvshost - MVS host (optional, default: oldcomputernerd.com)")
|
|
|
|
|
print()
|
|
|
|
|
print("Examples:")
|
|
|
|
|
print(" mvs_job.py src/sieve11.c")
|
|
|
|
|
print(" mvs_job.py src/sieve11.c '@05054.C90.SOURCE(SIEVE11)'")
|
|
|
|
|
print(" mvs_job.py src/hello.c '@05054.C90.SOURCE(HELLO)' mainframe.example.com")
|
|
|
|
|
print()
|
|
|
|
|
print("Notes:")
|
|
|
|
|
print(" - JCL file is assumed to be jcl/<basename>.jcl")
|
|
|
|
|
print(" - Member name defaults to source filename without extension")
|
2025-10-19 12:48:47 +00:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
local_file = sys.argv[1]
|
2026-02-06 09:29:52 +00:00
|
|
|
|
|
|
|
|
# Extract base name without extension for defaults
|
|
|
|
|
basename = os.path.splitext(os.path.basename(local_file))[0].upper()
|
|
|
|
|
valid_host_source_pds_suffixes = ['C', 'ALG', 'ASM', 'BAS', 'COB', 'PAS', 'PL360']
|
|
|
|
|
default_suffix = valid_host_source_pds_suffixes[0]
|
|
|
|
|
|
|
|
|
|
# Parse destination PDS (optional second argument)
|
|
|
|
|
if len(sys.argv) > 2 and sys.argv[2]:
|
|
|
|
|
destination = sys.argv[2]
|
|
|
|
|
# Parse PDS syntax: DATASET(MEMBER)
|
|
|
|
|
if '(' in destination and destination.endswith(')'):
|
|
|
|
|
dataset_name = destination[:destination.index('(')]
|
|
|
|
|
member_name = destination[destination.index('(')+1:-1]
|
|
|
|
|
else:
|
|
|
|
|
print(f"Error: Invalid PDS syntax '{destination}'. Use format: DATASET(MEMBER)")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
else:
|
|
|
|
|
# Default destination
|
|
|
|
|
dataset_name = f"@05054.SRCLIB.{default_suffix}"
|
2026-02-06 10:10:47 +00:00
|
|
|
member_name = basename.upper()
|
2026-02-06 09:29:52 +00:00
|
|
|
|
|
|
|
|
# JCL job name defaults to basename
|
2026-02-06 10:10:47 +00:00
|
|
|
job = basename.upper()
|
2026-02-06 09:29:52 +00:00
|
|
|
|
|
|
|
|
# Optional host override
|
|
|
|
|
mvshost = sys.argv[3] if len(sys.argv) > 3 else MVSHOST
|
|
|
|
|
|
|
|
|
|
print(f"Source: {local_file}")
|
|
|
|
|
print(f"Destination: {dataset_name}({member_name})")
|
|
|
|
|
print(f"JCL: jcl/{job}.jcl")
|
|
|
|
|
print(f"Host: {mvshost}")
|
|
|
|
|
print()
|
2025-10-19 12:48:47 +00:00
|
|
|
|
|
|
|
|
# Step 1: Upload source to PDS
|
|
|
|
|
if upload_source(local_file, dataset_name, member_name, mvshost) != 0:
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
2026-02-06 16:50:30 +00:00
|
|
|
# Wait between submissions to avoid overwhelming the card reader
|
|
|
|
|
print("Waiting 3 seconds before submitting compile job...")
|
|
|
|
|
time.sleep(3)
|
|
|
|
|
|
2025-10-19 12:48:47 +00:00
|
|
|
# Step 2: Submit JCL job
|
2026-02-06 16:50:30 +00:00
|
|
|
exit_code = submit_jcl(job, mvshost)
|
|
|
|
|
sys.exit(exit_code)
|