diff --git a/jac/jaclang/compiler/absyntree.py b/jac/jaclang/compiler/absyntree.py index e25ba9d81c..85d91fa97e 100644 --- a/jac/jaclang/compiler/absyntree.py +++ b/jac/jaclang/compiler/absyntree.py @@ -1013,8 +1013,9 @@ def resolve_relative_path(self, target_item: Optional[str] = None) -> str: target = self.dot_path_str if target_item: target += f".{target_item}" - base_path = os.path.dirname(self.loc.mod_path) - base_path = base_path if base_path else os.getcwd() + base_path = ( + os.getenv("JACPATH") or os.path.dirname(self.loc.mod_path) or os.getcwd() + ) parts = target.split(".") traversal_levels = self.level - 1 if self.level > 0 else 0 actual_parts = parts[traversal_levels:] @@ -1026,6 +1027,15 @@ def resolve_relative_path(self, target_item: Optional[str] = None) -> str: if os.path.exists(relative_path + ".jac") else relative_path ) + jacpath = os.getenv("JACPATH") + if not os.path.exists(relative_path) and jacpath: + name_to_find = actual_parts[-1] + ".jac" + + # Walk through the single path in JACPATH + for root, _, files in os.walk(jacpath): + if name_to_find in files: + relative_path = os.path.join(root, name_to_find) + break return relative_path def normalize(self, deep: bool = False) -> bool: diff --git a/jac/jaclang/compiler/passes/main/import_pass.py b/jac/jaclang/compiler/passes/main/import_pass.py index 618f987a22..5af9cc0e52 100644 --- a/jac/jaclang/compiler/passes/main/import_pass.py +++ b/jac/jaclang/compiler/passes/main/import_pass.py @@ -186,7 +186,7 @@ def import_jac_mod_from_file(self, target: str) -> ast.Module | None: self.warnings_had += mod_pass.warnings_had mod = mod_pass.ir except Exception as e: - logger.info(e) + logger.error(e) mod = None if isinstance(mod, ast.Module): self.import_table[target] = mod diff --git a/jac/jaclang/compiler/tests/test_importer.py b/jac/jaclang/compiler/tests/test_importer.py index 40fe79a9e7..e14bae40cd 100644 --- a/jac/jaclang/compiler/tests/test_importer.py +++ b/jac/jaclang/compiler/tests/test_importer.py @@ -62,3 +62,47 @@ def test_jac_py_import_auto(self) -> None: "{SomeObj(a=10): 'check'} [MyObj(apple=5, banana=7), MyObj(apple=5, banana=7)]", stdout_value, ) + + def test_import_with_jacpath(self) -> None: + """Test module import using JACPATH.""" + # Set up a temporary JACPATH environment variable + import os + import tempfile + + jacpath_dir = tempfile.TemporaryDirectory() + os.environ["JACPATH"] = jacpath_dir.name + + # Create a mock Jac file in the JACPATH directory + module_name = "test_module" + jac_file_path = os.path.join(jacpath_dir.name, f"{module_name}.jac") + with open(jac_file_path, "w") as f: + f.write( + """ + with entry { + "Hello from JACPATH!" :> print; + } + """ + ) + + # Capture the output + captured_output = io.StringIO() + sys.stdout = captured_output + + try: + JacMachine(self.fixture_abs_path(__file__)).attach_program( + JacProgram(mod_bundle=None, bytecode=None, sem_ir=None) + ) + jac_import(module_name, base_path=__file__) + cli.run(jac_file_path) + + # Reset stdout and get the output + sys.stdout = sys.__stdout__ + stdout_value = captured_output.getvalue() + + self.assertIn("Hello from JACPATH!", stdout_value) + + finally: + captured_output.close() + JacMachine.detach() + os.environ.pop("JACPATH", None) + jacpath_dir.cleanup() diff --git a/jac/jaclang/runtimelib/importer.py b/jac/jaclang/runtimelib/importer.py index c865da6603..83580e155f 100644 --- a/jac/jaclang/runtimelib/importer.py +++ b/jac/jaclang/runtimelib/importer.py @@ -158,9 +158,6 @@ def load_jac_mod_as_item( return getattr(new_module, name, new_module) except ImportError as e: logger.error(dump_traceback(e)) - # logger.error( - # f"Failed to load {name} from {jac_file_path} in {module.__name__}: {str(e)}" - # ) return None @@ -319,6 +316,32 @@ def run_import( """Run the import process for Jac modules.""" unique_loaded_items: list[types.ModuleType] = [] module = None + # Gather all possible search paths + jacpaths = os.environ.get("JACPATH", "") + search_paths = [spec.caller_dir] + if jacpaths: + for p in jacpaths.split(os.pathsep): + p = p.strip() + if p and p not in search_paths: + search_paths.append(p) + + # Attempt to locate the module file or directory + found_path = None + target_path_components = spec.target.split(".") + for search_path in search_paths: + candidate = os.path.join(search_path, "/".join(target_path_components)) + # Check if the candidate is a directory or a .jac file + if (os.path.isdir(candidate)) or (os.path.isfile(candidate + ".jac")): + found_path = candidate + break + + # If a suitable path was found, update spec.full_target; otherwise, raise an error + if found_path: + spec.full_target = os.path.abspath(found_path) + else: + raise ImportError( + f"Unable to locate module '{spec.target}' in {search_paths}" + ) if os.path.isfile(spec.full_target + ".jac"): module_name = self.get_sys_mod_name(spec.full_target + ".jac") module_name = spec.override_name if spec.override_name else module_name diff --git a/jac/jaclang/runtimelib/machine.py b/jac/jaclang/runtimelib/machine.py index 81d0256e69..0a85e0c012 100644 --- a/jac/jaclang/runtimelib/machine.py +++ b/jac/jaclang/runtimelib/machine.py @@ -38,6 +38,7 @@ def __init__(self, base_path: str = "") -> None: self.loaded_modules: dict[str, types.ModuleType] = {} if not base_path: base_path = os.getcwd() + # Ensure the base_path is a list rather than a string self.base_path = base_path self.base_path_dir = ( os.path.dirname(base_path) @@ -306,12 +307,11 @@ def get_bytecode( return marshal.load(f) result = compile_jac(full_target, cache_result=cachable) - if result.errors_had or not result.ir.gen.py_bytecode: + if result.errors_had: for alrt in result.errors_had: # We're not logging here, it already gets logged as the errors were added to the errors_had list. # Regardless of the logging, this needs to be sent to the end user, so we'll printing it to stderr. logger.error(alrt.pretty_print()) - return None if result.ir.gen.py_bytecode is not None: return marshal.loads(result.ir.gen.py_bytecode) else: