Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Added a new feature for IASP support.
* Fri Jan 23 2026 Het Patel <het.patel@ibm.com> - 3.1.1
- Added to escape library names containing the # symbol.
- Fixed source name case sensitivity in `makei compile` and `rules.mk`
Expand Down
1 change: 0 additions & 1 deletion docs/cli/crtfrmstmf.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,3 @@ usage: crtfrmstmf [-h] -f <srcstmf> -o <object> [-l <library>] -c <cmd> [-p [<pa
- **--save-joblog**

Output the joblog to the specified json file.

12 changes: 12 additions & 0 deletions docs/cli/makei.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ makei cvtsrcpf [-h] [-c <ccsid>] [-l] [-t] <file> <library>

Converts all members in a source physical file to properly-named (TOBi-compatible), UTF-8 encoded, LF-terminated source files in the current directory in the IFS. Generally speaking, the source member type will become the filename extension.

> [!IMPORTANT]
> **IASP Prerequisite:**
>
> If you are converting source files from a library in an IASP, you **must** first set the ASP group in your terminal session before running `makei cvtsrcpf`:
>
> ```bash
> cl "SETASPGRP ASPGRP(IASP1)"
> makei cvtsrcpf -c 37 -t -l QRPGLESRC IASPT1
> ```
>
> Replace `IASP1` with your actual IASP name. Without setting the ASP group first, the command will not be able to access libraries in the IASP.

For example, RPGLE source member `AB1001` will become IFS source file `AB1001.RPGLE`. Four exceptions exist, however: source member types CMD, MENU, and PNLGRP result in filename extensions .CMDSRC, .MENUSRC, and .PNLGRPSRC, respectively, and source member type C residing in source physical file H results in filename extension .H.

All source files will be encoded in UTF-8. If the source physical file was created successfully, a `.ibmi.json` file with the CCSID value from the SRC-PF will be created in the same directory. Note that it will not override an existing `.ibmi.json` file. [Link to discussions](https://github.com/IBM/ibmi-tobi/pull/115#issuecomment-1194661949)
Expand Down
9 changes: 8 additions & 1 deletion docs/prepare-the-project/convert-source-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ By default the source files will be encoded as UTF-8, but you may use `-c` optio

Before running this tool, verify that the CCSID of the source physical file is set correctly; a value of 65535 can result in an improper conversion.

> [!IMPORTANT]
> **For IASP Libraries:** If your source physical file resides in an IASP library, you must first set the ASP group in your terminal before running the conversion:
> ```bash
> cl "SETASPGRP ASPGRP(IASP1)"
> ```
> Replace `IASP1` with your actual IASP name.

[How to use `makei cvtsrcpf` command](cli/makei?id=cvtsrcpf)

There are a couple of other file extension changes that need to be made after converting to stream files. Any `.RPGLE` files that are included should be renamed to `.RPGLEINC` so that TOBi knows not to compile them. It is ambiguous for TOBi to know whether an ILE source is intended to be compiled into a MODULE, PGM or SRVPGM. TOBi will assume that a MODULE is the default. If a PGM object is the target using CRTBNDxxx then the file extension should be `PGM.xxx` i.e. PGM.RPGLE for ILE RPG. As of version 2.4.33 this is suggested by not required as TOBi infers the relationship from the Rules.mk file. i.e. `FOO.PGM: foo.rpgle` would tell it to use `CRTBNDRPG`. See [Supported object types](welcome/features.md?id=supported-object-types) for more discussion of this.
Expand All @@ -32,4 +39,4 @@ Finally if there are any objects that are not compiled, they can be represented

Now you have converted the source files into an IFS directory. Make sure put them in a TOBi project after conversion. You will need some special files to enable TOBi. This is fully described in [Project metadata](project-metadata) and [Create a New Project](prepare-the-project/create-a-new-project).

If you are concerned about national characters or supporting multiple EBCDIC encodings in the same project see [Encoding source files](prepare-the-project/encoding-source-code)
If you are concerned about national characters or supporting multiple EBCDIC encodings in the same project see [Encoding source files](prepare-the-project/encoding-source-code)
23 changes: 22 additions & 1 deletion docs/prepare-the-project/iproj-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,27 @@ license for this project

### iasp

the Independent Auxiliary Storage Pool that should be used when developing this project. If not specified, it is assumed to be "*SYSBAS".
The Independent Auxiliary Storage Pool that should be used when developing this project. If not specified, it is assumed to be "*SYSBAS".

>[!IMPORTANT]
>**IASP Configuration Requirements:**
>
> When working with an IASP, you must configure **both** of the following in your `iproj.json`:
>
> 1. **Set the `iasp` field** to your IASP name (e.g., `"iasp": "IASP1"`)
> 2. **Set the `setIBMiEnvCmd` field** to include the `SETASPGRP` command and export `CURLIB` as your IASP library
>
> **Example IASP configuration:**
> ```json
> {
> "iasp": "IASP1",
> "curlib": "MYLIB",
> "setIBMiEnvCmd": [
> "SETASPGRP ASPGRP(IASP1)"]
> }
> ```
>
> Without both settings, TOBi will not be able to properly access libraries and objects in the IASP.

### sql

Expand Down Expand Up @@ -139,6 +159,7 @@ In this example we have a project with the description of Payroll application
"preUsrlibl": "&accounting, ACMEUTIL",
"postUsrlibl": "&tax",
"setIBMiEnvCmd": ["CALL &accounting/SETUP"],
"iasp": "IASP1",
"repository" : "https://github.com/acme/backoffice",
"extensions": {
"arcad": {
Expand Down
7 changes: 5 additions & 2 deletions src/makei/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def __init__(self, targets: List[str] = None, make_options: Optional[str] = None
self.iproj_json_path = self.src_dir / "iproj.json"
self.iproj_json = IProjJson.from_file(self.iproj_json_path)
self.color = support_color()

self.iasp = self.iproj_json.iasp
if len(self.iproj_json.set_ibm_i_env_cmd) > 0:
cmd_list = self.iproj_json.set_ibm_i_env_cmd
self.ibmi_env_cmds = "\\n".join(cmd_list)
Expand Down Expand Up @@ -166,6 +166,7 @@ def map_ibmi_json_var(path):
incdir = '\'' + '\' \''.join(include_path) + '\''
elif len(include_path) == 1:
incdir = include_path[0].upper()

with target_file_path.open("w", encoding="utf8") as file:
# Library names that include the hash symbol need to be
# escaped otherwise make will treat characters after the
Expand All @@ -185,6 +186,7 @@ def map_ibmi_json_var(path):
unquotedINCDIR := {' '.join(include_path)}
doublequotedINCDIR := {incdir.replace("'", "''")}
IBMiEnvCmd := {self.ibmi_env_cmds}
iasp := {self.iasp}
COLOR_TTY := {'true' if self.color else 'false'}

""")
Expand All @@ -193,7 +195,8 @@ def map_ibmi_json_var(path):
file.write(
f"TGTCCSID_{subdir.absolute()} := {dir_var_map[subdir].build['tgt_ccsid']}\n")
file.write(
f"OBJPATH_{subdir.absolute()} := {objlib_to_path(dir_var_map[subdir].build['objlib'])}\n")
f"OBJPATH_{subdir.absolute()} := "
f"{objlib_to_path(dir_var_map[subdir].build['objlib'], iasp=self.iasp)}\n")

# for rules_mk in rules_mks:
# with rules_mk.open('r') as rules_mk_file:
Expand Down
8 changes: 7 additions & 1 deletion src/makei/cli/makei_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from makei.cvtsrcpf import CvtSrcPf
from makei.utils import Colors, colored, decompose_filename
from pathlib import Path
from makei.iproj_json import IProjJson


def cli():
Expand Down Expand Up @@ -314,7 +315,12 @@ def handle_cvtsrcpf(args):
"""
if args.log:
print(colored("Warning: --trace has no effect on 'cvtsrcpf' command.", Colors.WARNING))
CvtSrcPf(args.file, args.library, args.tolower, args.ccsid, args.text).run()
iasp = ""
iproj_path = Path.cwd() / "iproj.json"
if iproj_path.exists():
iproj = IProjJson.from_file(iproj_path)
iasp = iproj.iasp
CvtSrcPf(args.file, args.library, args.tolower, args.ccsid, args.text, iasp=iasp).run()


def get_override_vars(args):
Expand Down
2 changes: 1 addition & 1 deletion src/makei/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
DEFAULT_TGT_CCSID = "*JOB"
DEFAULT_OBJLIB = "*CURLIB"
DEFAULT_CURLIB = "*CRTDFT"

DEFAULT_IASP = ""
TOBI_PATH = Path(__file__).resolve().parent.parent.parent
MK_PATH = TOBI_PATH / "src" / "mk"

Expand Down
76 changes: 49 additions & 27 deletions src/makei/crtfrmstmf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from makei.ibm_job import IBMJob, save_joblog_json
from makei.utils import format_datetime, objlib_to_path, validate_ccsid, make_include_dirs_absolute
from makei.utils import format_datetime, objlib_to_path, validate_ccsid, make_include_dirs_absolute, get_iasp_prefix

COMMAND_MAP = {'CRTCMD': 'CMD',
'CRTBNDCL': 'PGM',
Expand Down Expand Up @@ -49,11 +48,12 @@ class CrtFrmStmf():
obj_type: str
precmd: str
postcmd: str
iasp: str

def __init__(self, srcstmf: str, obj: str, lib: str, cmd: str, rcdlen: int, tgt_ccsid: Optional[str] = None,
parameters: Optional[str] = None, env_settings: Optional[Dict[str, str]] = None,
joblog_path: Optional[str] = None, tmp_lib="QTEMP", tmp_src="QSOURCE", precmd="",
postcmd="", output="") -> None:
joblog_path: Optional[str] = None, tmp_lib=None, tmp_src="QSOURCE", precmd="",
postcmd="", output="", iasp: str = "") -> None:
# pylint: disable=too-many-arguments
self.job = IBMJob()
self.setup_job = IBMJob()
Expand All @@ -66,13 +66,14 @@ def __init__(self, srcstmf: str, obj: str, lib: str, cmd: str, rcdlen: int, tgt_
self.env_settings = env_settings if env_settings is not None else {}
self.joblog_path = joblog_path
self.job.run_cl("CHGJOB LOG(4 00 *SECLVL)", log=False)
self.tmp_lib = tmp_lib
self.tmp_src = tmp_src
self.obj_type = COMMAND_MAP[self.cmd]
self.precmd = precmd
self.postcmd = postcmd
self.output = output

self.iasp = iasp if iasp else ""
self.iasp_prefix = get_iasp_prefix(self.iasp)
self.tmp_lib = resolve_tmp_lib(self.lib, self.iasp)
if tgt_ccsid is None or not validate_ccsid(tgt_ccsid):
ccsid = retrieve_ccsid(srcstmf)
if ccsid in ["1208", "819"]:
Expand All @@ -82,10 +83,11 @@ def __init__(self, srcstmf: str, obj: str, lib: str, cmd: str, rcdlen: int, tgt_
else:
self.ccsid_c = tgt_ccsid

if check_object_exists(self.obj, self.lib, self.obj_type):
if check_object_exists(self.obj, self.lib, self.obj_type, self.iasp):
if self.cmd == "CRTPF":
# For physical files, delete all its logical file dependencies
self.back_up_obj_list = get_physical_dependencies(self.obj, self.lib, True, self.setup_job)
self.back_up_obj_list = get_physical_dependencies(
self.obj, self.lib, True, self.setup_job, iasp=self.iasp)
else:
self.back_up_obj_list = [(self.obj, self.lib, self.obj_type)]
else:
Expand All @@ -101,7 +103,6 @@ def run(self):
self.setup_env()

run_datetime = datetime.now()

# Run the pre_cmd
if self.precmd:
self.job.run_cl(self.precmd, False, True)
Expand All @@ -114,7 +115,8 @@ def run(self):
# Copy the source stream file to the temp source file
self.job.run_cl(
f'CPYFRMSTMF FROMSTMF("{self.srcstmf}") '
f'TOMBR("/QSYS.LIB/{self.tmp_lib}.LIB/{self.tmp_src}.FILE/{self.obj}.MBR") MBROPT(*REPLACE)')
f'TOMBR("{self.iasp_prefix}/QSYS.LIB/{self.tmp_lib}.LIB/{self.tmp_src}.FILE/{self.obj}.MBR") '
f'MBROPT(*REPLACE)')

self._backup_and_delete_objs()

Expand Down Expand Up @@ -150,6 +152,7 @@ def run(self):
return success

def setup_env(self):

if "curlib" in self.env_settings and self.env_settings["curlib"]:
self.job.run_cl(f"CHGCURLIB CURLIB({self.env_settings['curlib']})", log=True)

Expand All @@ -163,7 +166,8 @@ def setup_env(self):

if "IBMiEnvCmd" in self.env_settings and self.env_settings["IBMiEnvCmd"]:
for cmd in self.env_settings["IBMiEnvCmd"].split("\\n"):
self.job.run_cl(cmd, log=True)
if "SETASPGRP" not in cmd.upper():
self.job.run_cl(cmd, log=True)

def _retrieve_current_library(self):
records, _ = self.job.run_sql(
Expand All @@ -174,31 +178,33 @@ def _retrieve_current_library(self):
return "*NONE"

def _update_event_file(self, ccsid):
alias_name = f"{self.obj}_EVFALIAS"

self.setup_job.run_sql(
f"CREATE OR REPLACE ALIAS {self.tmp_lib}.{self.obj} FOR {self.lib}.EVFEVENT ({self.obj});")
f"CREATE OR REPLACE ALIAS {self.tmp_lib}.{alias_name} FOR {self.lib}.EVFEVENT ({self.obj});")
results = self.setup_job.run_sql(" ".join(["SELECT",
f"CAST(EVFEVENT AS VARCHAR(300) CCSID {ccsid}) AS FULL",
f"FROM {self.tmp_lib}.{self.obj}",
f"FROM {self.tmp_lib}.{alias_name}",
f"WHERE Cast(evfevent As Varchar(300) Ccsid {ccsid})",
f"LIKE 'FILEID%{self.tmp_lib}/{self.tmp_src}({self.obj})%'",
]))[0]
if results:
parts = results[0][0].split()
else:
return
self.setup_job.run_sql(" ".join([f"Update {self.tmp_lib}.{self.obj}",
self.setup_job.run_sql(" ".join([f"Update {self.tmp_lib}.{alias_name}",
"Set evfevent =",
"(",
f"SELECT Cast(evfevent As Varchar(24) Ccsid {ccsid})",
f"CONCAT '{len(self.srcstmf):03} {self.srcstmf} {parts[-2]} {parts[-1]}'",
f"FROM {self.tmp_lib}.{self.obj}",
f"FROM {self.tmp_lib}.{alias_name}",
f"WHERE Cast(evfevent As Varchar(300) Ccsid {ccsid})",
f"LIKE 'FILEID%{self.tmp_lib}/{self.tmp_src}({self.obj})%'",
"FETCH First 1 Row Only)",
f"WHERE Cast(evfevent As Varchar(300) Ccsid {ccsid})",
f"LIKE 'FILEID%{self.tmp_lib}/{self.tmp_src}({self.obj})%'"]))

self.setup_job.run_sql(f"DROP ALIAS {self.tmp_lib}.{self.obj}")
self.setup_job.run_sql(f"DROP ALIAS {self.tmp_lib}.{alias_name}")

def _backup_and_delete_objs(self):
obj_list = self.back_up_obj_list
Expand Down Expand Up @@ -334,17 +340,19 @@ def cli():
env_settings["postUsrlibl"] = sanitize_lib_envvar(os.environ["postUsrlibl"])
if "IBMiEnvCmd" in os.environ:
env_settings["IBMiEnvCmd"] = os.environ["IBMiEnvCmd"]
if "iasp" in os.environ:
env_settings["iasp"] = os.environ["iasp"]

iasp_name = env_settings.get("iasp", "")
handle = CrtFrmStmf(srcstmf_absolute_path, args.object.strip(),
args.library.strip(), args.command.strip(), args.rcdlen, args.ccsid, args.parameters,
env_settings, args.save_joblog, precmd=args.precmd, postcmd=args.postcmd, output=args.output)

args.library.strip(), args.command.strip(), args.rcdlen, args.ccsid,
args.parameters, env_settings, args.save_joblog, precmd=args.precmd,
postcmd=args.postcmd, output=args.output, iasp=iasp_name)
print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
success = handle.run()
print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<")
sys.exit(0 if success else 1)


# Helper functions


Expand Down Expand Up @@ -375,13 +383,14 @@ def retrieve_ccsid(srcstmf: str) -> str:
return _get_attr(srcstmf)["CCSID"]


def check_object_exists(obj: str, lib: str, obj_type: str) -> bool:
obj_path = Path(f"/QSYS.LIB/{lib}.LIB/{obj}.{obj_type}")
def check_object_exists(obj: str, lib: str, obj_type: str, iasp: str = "") -> bool:
iasp_prefix = get_iasp_prefix(iasp)
obj_path = Path(f"{iasp_prefix}/QSYS.LIB/{lib}.LIB/{obj}.{obj_type}")
return obj_path.exists()


def get_physical_dependencies(obj: str, lib: str, include_self: bool, job: Optional[IBMJob] = None,
verbose: bool = False) -> List[Tuple[str, str, str]]:
verbose: bool = False, iasp: str = "") -> List[Tuple[str, str, str]]:
"""Get the dependencies for a given physical file object

Args:
Expand All @@ -390,13 +399,14 @@ def get_physical_dependencies(obj: str, lib: str, include_self: bool, job: Optio
include_self (bool): whether to include the physical file itself in the result
job (IBMJob, optional): Job used to run the commands. If none is set, a new job will be created. Defaults to
None.
verbose (bool, optional): Defaults to False.
verbose (bool, optional): Defaults to False
iasp (str, optional): IASP name. Defaults to "".

Returns:
List[Tuple[str, str, str]]: List of (obj, lib, obj_type) tuples
"""

lib_path = Path(f'/QSYS.LIB/{lib}.LIB')
iasp_prefix = get_iasp_prefix(iasp)
lib_path = Path(f'{iasp_prefix}/QSYS.LIB/{lib}.LIB')
pf_path = lib_path / f"{obj}.FILE"
if not pf_path.exists():
if verbose:
Expand Down Expand Up @@ -433,10 +443,22 @@ def delete_objects(obj_list: List[Tuple[str, str, str]], verbose: bool = False):
print(f"{obj_path} not deleted.")


def is_iasp_library(lib: str, iasp: str = "") -> bool:
if not iasp:
return False
lib_path = Path(f"/{iasp}/QSYS.LIB/{lib}.LIB")
return lib_path.exists()


def resolve_tmp_lib(lib: str, iasp: str = "") -> str:
if is_iasp_library(lib, iasp):
return lib
return "QTEMP"


def filter_joblogs(record: Dict[str, Any]) -> bool:
"""Filter out the joblog records that are not related to the compile job"""
# pylint: disable=too-many-return-statements

msgid = record["MESSAGE_ID"]
msgtext = record["MESSAGE_TEXT"]
if msgid is None:
Expand Down
9 changes: 5 additions & 4 deletions src/makei/cvtsrcpf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,17 @@ class CvtSrcPf:
tolower: bool
ibmi_json_path: Optional[Path]
store_member_text: bool
iasp: str

def __init__(
self, srcfile: str, lib: str, tolower: bool, default_ccsid: str = None, text: bool = False,
save_path: Path = Path.cwd()
) -> None:
self, srcfile: str, lib: str, tolower: bool, default_ccsid: str = None,
text: bool = False, save_path: Path = Path.cwd(), iasp: str = "") -> None:
self.job = IBMJob()

self.lib = lib
self.srcfile = srcfile
self.save_path = save_path
self.iasp = iasp
if default_ccsid is not None and validate_ccsid(default_ccsid):
self.default_ccsid = default_ccsid
else:
Expand Down Expand Up @@ -91,7 +92,7 @@ def import_member_text(self, file_path: str, member_text: str) -> bool:
return False

def run(self) -> int:
srcpath = Path(objlib_to_path(self.lib, f"{self.srcfile}.FILE"))
srcpath = Path(objlib_to_path(self.lib, f"{self.srcfile}.FILE", self.iasp))
if not srcpath.exists():
raise Exception(f"Source file '{srcpath}' does not exist")
src_mbrs = self._get_src_mbrs()
Expand Down
Loading