diff --git a/scripts/_build_helper.py b/scripts/_build_helper.py index f20802fabb..6d3128ffbf 100644 --- a/scripts/_build_helper.py +++ b/scripts/_build_helper.py @@ -20,23 +20,11 @@ import sys -def run(cmd, env=None, cwd=None): - print("+", " ".join(cmd)) - subprocess.check_call( - cmd, env=env or os.environ.copy(), cwd=cwd or os.getcwd() - ) - - -def warn(msg: str): - print(f"[build_locally][error] {msg}", file=sys.stderr) - - -def err(msg: str): - print(f"[build_locally][error] {msg}", file=sys.stderr) - - def resolve_compilers( - oneapi: bool, c_compiler: str, cxx_compiler: str, compiler_root: str + oneapi: bool, + c_compiler: str, + cxx_compiler: str, + compiler_root: str, ): is_linux = "linux" in sys.platform @@ -45,10 +33,14 @@ def resolve_compilers( ): return "icx", ("icpx" if is_linux else "icx") - if not compiler_root or not os.path.exists(compiler_root): + if ( + (c_compiler is None or not os.path.isabs(c_compiler)) + and (cxx_compiler is None or not os.path.isabs(cxx_compiler)) + and (not compiler_root or not os.path.exists(compiler_root)) + ): raise RuntimeError( "--compiler-root option must be set when using non-default DPC++ " - "layout" + "layout unless absolute paths are provided for both compilers" ) # default values @@ -61,34 +53,54 @@ def resolve_compilers( (c_compiler, "--c-compiler"), (cxx_compiler, "--cxx-compiler"), ): - path = ( - name if os.path.exists(name) else os.path.join(compiler_root, name) - ) + if os.path.isabs(name): + path = name + else: + path = os.path.join(compiler_root, name) if not os.path.exists(path): raise RuntimeError(f"{opt_name} value {name} not found") return c_compiler, cxx_compiler +def run(cmd: list[str], env: dict[str, str] = None, cwd: str = None): + print("+", " ".join(cmd)) + subprocess.check_call( + cmd, env=env or os.environ.copy(), cwd=cwd or os.getcwd() + ) + + +def get_output(cmd: list[str], cwd: str = None): + print("+", " ".join(cmd)) + return ( + subprocess.check_output(cmd, cwd=cwd or os.getcwd()) + .decode("utf-8") + .strip("\n") + ) + + +def warn(msg: str, script: str): + print(f"[{script}][warning] {msg}", file=sys.stderr) + + +def err(msg: str, script: str): + print(f"[{script}][error] {msg}", file=sys.stderr) + + def make_cmake_args( - build_type="Release", - c_compiler=None, - cxx_compiler=None, - level_zero=True, - glog=False, - generator=None, - verbose=False, - other_opts="", + c_compiler: str = None, + cxx_compiler: str = None, + level_zero: bool = True, + glog: bool = False, + verbose: bool = False, + other_opts: str = None, ): args = [ - f"-DCMAKE_BUILD_TYPE={build_type}", f"-DCMAKE_C_COMPILER:PATH={c_compiler}" if c_compiler else "", f"-DCMAKE_CXX_COMPILER:PATH={cxx_compiler}" if cxx_compiler else "", f"-DDPCTL_ENABLE_L0_PROGRAM_CREATION={'ON' if level_zero else 'OFF'}", f"-DDPTL_ENABLE_GLOG:BOOL={'ON' if glog else 'OFF'}", ] - if generator: - args.append(f"-G{generator}") if verbose: args.append("-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON") if other_opts: @@ -97,15 +109,28 @@ def make_cmake_args( return " ".join(filter(None, args)) -def build_extension(setup_dir, env): +def build_extension( + setup_dir: str, + env: dict[str, str], + cmake_executable: str = None, + generator: str = None, + build_type: str = None, +): + cmd = [sys.executable, "setup.py", "build_ext", "--inplace"] + if cmake_executable: + cmd.append(f"--cmake-executable={cmake_executable}") + if generator: + cmd.append(f"--generator={generator}") + if build_type: + cmd.append(f"--build-type={build_type}") run( - [sys.executable, "setup.py", "build_ext", "--inplace"], + cmd, env=env, cwd=setup_dir, ) -def install_editable(setup_dir, env): +def install_editable(setup_dir: str, env: dict[str, str]): run( [ sys.executable, @@ -121,7 +146,7 @@ def install_editable(setup_dir, env): ) -def clean_build_dir(setup_dir): +def clean_build_dir(setup_dir: str = None): target = os.path.join(setup_dir or os.getcwd(), "_skbuild") if os.path.exists(target): print(f"Cleaning build directory: {target}") diff --git a/scripts/build_locally.py b/scripts/build_locally.py index 5bfb487e4d..76c7c61eb5 100644 --- a/scripts/build_locally.py +++ b/scripts/build_locally.py @@ -124,7 +124,7 @@ def parse_args(): p.add_argument( "--clean", action="store_true", - help="Remove build dir before rebuild (default: False)", + help="Remove build dir before rebuild", ) p.add_argument( "--skip-editable", @@ -137,7 +137,7 @@ def parse_args(): def main(): if sys.platform not in ["cygwin", "win32", "linux"]: - err(f"{sys.platform} not supported") + err(f"{sys.platform} not supported", "build_locally") args = parse_args() setup_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -150,7 +150,10 @@ def main(): clean_build_dir(setup_dir) if args.no_level_zero and args.target_level_zero: - err("Cannot combine --no-level-zero and --target-level-zero") + err( + "Cannot combine --no-level-zero and --target-level-zero", + "build_locally", + ) # Level Zero state (on unless explicitly disabled) if args.no_level_zero: @@ -161,19 +164,17 @@ def main(): level_zero_enabled = True cmake_args = make_cmake_args( - build_type=args.build_type, c_compiler=c_compiler, cxx_compiler=cxx_compiler, level_zero=level_zero_enabled, glog=args.glog, - generator=args.generator, verbose=args.verbose, other_opts=args.cmake_opts, ) # handle architecture conflicts if args.target_hip is not None and not args.target_hip.strip(): - err("--target-hip requires an explicit architecture") + err("--target-hip requires an explicit architecture", "build_locally") # CUDA/HIP targets if args.target_cuda: @@ -190,7 +191,7 @@ def main(): # ignore pre-existing CMAKE_ARGS for determinism in build driver if "CMAKE_ARGS" in env and env["CMAKE_ARGS"].strip(): - warn("Ignoring pre-existing CMAKE_ARGS in environment") + warn("Ignoring pre-existing CMAKE_ARGS in environment", "build_locally") del env["CMAKE_ARGS"] env["CMAKE_ARGS"] = cmake_args @@ -198,7 +199,13 @@ def main(): print("[build_locally] Building extensions in-place...") - build_extension(setup_dir, env) + build_extension( + setup_dir, + env, + cmake_executable=args.cmake_executable, + generator=args.generator, + build_type=args.build_type, + ) if not args.skip_editable: install_editable(setup_dir, env) else: diff --git a/scripts/gen_coverage.py b/scripts/gen_coverage.py index bdc2bd931c..7b203b924f 100644 --- a/scripts/gen_coverage.py +++ b/scripts/gen_coverage.py @@ -14,66 +14,195 @@ # See the License for the specific language governing permissions and # limitations under the License. +import argparse import os import re import subprocess import sys import sysconfig +# add scripts dir to Python path so we can import _build_helper +sys.path.insert(0, os.path.abspath("scripts")) -def run( - use_oneapi=True, - c_compiler=None, - cxx_compiler=None, - level_zero=True, - compiler_root=None, - run_pytest=False, - bin_llvm=None, - gtest_config=None, - verbose=False, -): - IS_LIN = False - - if "linux" in sys.platform: - IS_LIN = True - elif sys.platform in ["win32", "cygwin"]: - pass +from _build_helper import ( # noqa: E402 + build_extension, + clean_build_dir, + err, + get_output, + install_editable, + make_cmake_args, + resolve_compilers, + run, + warn, +) + + +def find_bin_llvm(compiler): + if os.path.isabs(compiler): + bin_dir = os.path.dirname(compiler) + else: + compiler_path = get_output(["which", compiler]) + if not compiler_path: + raise RuntimeError(f"Compiler {compiler} not found in PATH") + bin_dir = os.path.dirname(compiler_path) + compiler_dir = os.path.join(bin_dir, "compiler") + if os.path.exists(compiler_dir): + bin_llvm = compiler_dir else: - assert False, sys.platform + " not supported" + bin_dir = os.path.dirname(bin_dir) + bin_llvm = os.path.join(bin_dir, "bin-llvm") + if not os.path.exists(bin_llvm): + raise RuntimeError(f"--bin-llvm value {bin_llvm} not found") + return bin_llvm - if not IS_LIN: - raise RuntimeError( - "This scripts only supports coverage collection on Linux" - ) + +def parse_args(): + p = argparse.ArgumentParser(description="Build dpctl and generate coverage") + + p.add_argument( + "--c-compiler", default=None, help="Path or name of C compiler" + ) + p.add_argument( + "--cxx-compiler", default=None, help="Path or name of C++ compiler" + ) + p.add_argument( + "--compiler-root", + type=str, + default=None, + help="Path to compiler installation root", + ) + p.add_argument( + "--oneapi", + dest="oneapi", + action="store_true", + help="Use default oneAPI compiler layout", + ) + + p.add_argument( + "--verbose", + dest="verbose", + action="store_true", + help="Enable verbose makefile output", + ) + + p.add_argument( + "--no-level-zero", + dest="no_level_zero", + action="store_true", + default=False, + help="Disable Level Zero backend (deprecated: use --target-level-zero " + "OFF)", + ) + p.add_argument( + "--target-level-zero", + action="store_true", + help="Enable Level Zero backend explicitly", + ) + + p.add_argument( + "--generator", type=str, default="Ninja", help="CMake generator" + ) + p.add_argument( + "--cmake-executable", + type=str, + default=None, + help="Path to CMake executable used by build", + ) + + p.add_argument( + "--cmake-opts", + type=str, + default="", + help="Additional options to pass directly to CMake", + ) + + p.add_argument( + "--gtest-config", + help="Path to GTestConfig.cmake file for a custom GTest installation", + ) + p.add_argument( + "--bin-llvm", + help="Path to folder where llvm-cov/llvm-profdata can be found", + ) + p.add_argument("--skip-pytest", dest="run_pytest", action="store_false") + p.add_argument( + "--clean", + action="store_true", + help="Remove build dir before rebuild (default: False)", + ) + + return p.parse_args() + + +def main(): + is_linux = "linux" in sys.platform + if not is_linux: + err(f"{sys.platform} not supported", "gen_coverage") + args = parse_args() setup_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - cmake_args = [ - sys.executable, - "setup.py", - "develop", - "--build-type=Coverage", - "--generator=Ninja", - "--", - "-DCMAKE_C_COMPILER:PATH=" + c_compiler, - "-DCMAKE_CXX_COMPILER:PATH=" + cxx_compiler, - "-DDPCTL_ENABLE_L0_PROGRAM_CREATION=" + ("ON" if level_zero else "OFF"), - "-DDPCTL_GENERATE_COVERAGE=ON", - "-DDPCTL_BUILD_CAPI_TESTS=ON", - "-DDPCTL_COVERAGE_REPORT_OUTPUT_DIR=" + setup_dir, - ] - env = dict() + + c_compiler, cxx_compiler = resolve_compilers( + args.oneapi, + args.c_compiler, + args.cxx_compiler, + args.compiler_root, + ) + bin_llvm = find_bin_llvm(c_compiler) + + if args.clean: + clean_build_dir(setup_dir) + + if args.no_level_zero and args.target_level_zero: + err( + "Cannot combine --no-level-zero and --target-level-zero", + "gen_coverage", + ) + + # Level Zero state (on unless explicitly disabled) + if args.no_level_zero: + level_zero_enabled = False + elif args.target_level_zero: + level_zero_enabled = True + else: + level_zero_enabled = True + + cmake_args = make_cmake_args( + c_compiler=c_compiler, + cxx_compiler=cxx_compiler, + level_zero=level_zero_enabled, + verbose=args.verbose, + ) + + cmake_args += " -DDPCTL_GENERATE_COVERAGE=ON" + cmake_args += " -DDPCTL_BUILD_CAPI_TESTS=ON" + cmake_args += f" -DDPCTL_COVERAGE_REPORT_OUTPUT={setup_dir}" + + if args.gtest_config: + cmake_args += " -DCMAKE_PREFIX_PATH={args.gtest_config}" + + env = os.environ.copy() + + if "CMAKE_ARGS" in env and env["CMAKE_ARGS"].strip(): + warn("Ignoring pre-existing CMAKE_ARGS in environment", "gen_coverage") + del env["CMAKE_ARGS"] + if bin_llvm: - env = { - "PATH": ":".join((os.environ.get("PATH", ""), bin_llvm)), - "LLVM_TOOLS_HOME": bin_llvm, - } - env.update({k: v for k, v in os.environ.items() if k != "PATH"}) - if gtest_config: - cmake_args += ["-DCMAKE_PREFIX_PATH=" + gtest_config] - if verbose: - cmake_args += [ - "-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON", - ] - subprocess.check_call(cmake_args, shell=False, cwd=setup_dir, env=env) + env["PATH"] = ":".join((env.get("PATH", ""), bin_llvm)) + env["LLVM_TOOLS_HOME"] = bin_llvm + + env["CMAKE_ARGS"] = cmake_args + + print(f"[gen_coverage] Using CMake args:\n {env['CMAKE_ARGS']}") + + build_extension( + setup_dir, + env, + cmake_executable=args.cmake_executable, + generator=args.generator, + build_type="Coverage", + ) + install_editable(setup_dir, env) + cmake_build_dir = ( subprocess.check_output( ["find", "_skbuild", "-name", "cmake-build"], cwd=setup_dir @@ -81,13 +210,22 @@ def run( .decode("utf-8") .strip("\n") ) - subprocess.check_call( + + cmake_build_dir = get_output( + ["find", "_skbuild", "-name", "cmake-build"], + cwd=setup_dir, + ) + + print(f"[gen_coverage] Found CMake build dir: {cmake_build_dir}") + + run( ["cmake", "--build", ".", "--target", "llvm-cov-report"], cwd=cmake_build_dir, ) - env["LLVM_PROFILE_FILE"] = "dpctl_pytest.profraw" - subprocess.check_call( - [ + + if args.run_pytest: + env["LLVM_PROFILE_FILE"] = "dpctl_pytest.profraw" + pytest_cmd = [ "pytest", "-q", "-ra", @@ -103,147 +241,66 @@ def run( "-vv", "--ignore=dpctl/tensor/libtensor/tests", "--no-sycl-interface-test", - ], - cwd=setup_dir, - shell=False, - env=env, - ) - - def find_objects(): - import os - - objects = [] - sfx_regexp = sysconfig.get_config_var("EXT_SUFFIX").replace(".", r"\.") - regexp1 = re.compile(r"^_tensor_.*impl" + sfx_regexp) - regexp2 = re.compile(r"^^_device_queries" + sfx_regexp) - - def is_py_ext(fn): - return re.match(regexp1, fn) or re.match(regexp2, fn) - - for root, _, files in os.walk("dpctl"): - for file in files: - if not file.endswith(".so"): - continue - if is_py_ext(file) or file.find("DPCTLSyclInterface") != -1: - objects.extend(["-object", os.path.join(root, file)]) - print("Using objects: ", objects) - return objects - - objects = find_objects() - instr_profile_fn = "dpctl_pytest.profdata" - # generate instrumentation profile data - subprocess.check_call( - [ - os.path.join(bin_llvm, "llvm-profdata"), - "merge", - "-sparse", - env["LLVM_PROFILE_FILE"], - "-o", - instr_profile_fn, ] - ) - # export lcov - with open("dpctl_pytest.lcov", "w") as fh: - subprocess.check_call( + run(pytest_cmd, env=env, cwd=setup_dir) + + def find_objects(): + objects = [] + sfx_regexp = sysconfig.get_config_var("EXT_SUFFIX").replace( + ".", r"\." + ) + regexp1 = re.compile(r"^_tensor_.*impl" + sfx_regexp) + regexp2 = re.compile(r"^^_device_queries" + sfx_regexp) + + def is_py_ext(fn): + return re.match(regexp1, fn) or re.match(regexp2, fn) + + for root, _, files in os.walk("dpctl"): + for file in files: + if not file.endswith(".so"): + continue + if is_py_ext(file) or "DPCTLSyclInterface" in file: + objects.extend(["-object", os.path.join(root, file)]) + print("Using objects: ", objects) + return objects + + objects = find_objects() + instr_profile_fn = "dpctl_pytest.profdata" + + run( [ - os.path.join(bin_llvm, "llvm-cov"), - "export", - "-format=lcov", - "-ignore-filename-regex=/tmp/icpx*", - "-instr-profile=" + instr_profile_fn, + os.path.join(bin_llvm, "llvm-profdata"), + "merge", + "-sparse", + env["LLVM_PROFILE_FILE"], + "-o", + instr_profile_fn, ] - + objects, - stdout=fh, ) + with open("dpctl_pytest.lcov", "w") as fh: + subprocess.check_call( + [ + os.path.join(bin_llvm, "llvm-cov"), + "export", + "-format=lcov", + "-ignore-filename-regex=/tmp/icpx*", + f"-instr-profile={instr_profile_fn}", + ] + + objects, + cwd=setup_dir, + env=env, + stdout=fh, + ) + print("[gen_coverage] Coverage export complete: dpctl_pytest.lcov") + else: + print( + "[gen_coverage] Skipping pytest and coverage collection " + "(--skip-pytest)" + ) -if __name__ == "__main__": - import argparse + print("[gen_coverage] Done") - parser = argparse.ArgumentParser( - description="Driver to build dpctl and generate coverage" - ) - driver = parser.add_argument_group(title="Coverage driver arguments") - driver.add_argument("--c-compiler", help="Name of C compiler", default=None) - driver.add_argument( - "--cxx-compiler", help="Name of C++ compiler", default=None - ) - driver.add_argument( - "--not-oneapi", - help="Is one-API installation", - dest="oneapi", - action="store_false", - ) - driver.add_argument( - "--compiler-root", type=str, help="Path to compiler home directory" - ) - driver.add_argument( - "--no-level-zero", - help="Enable Level Zero support", - dest="level_zero", - action="store_false", - ) - driver.add_argument( - "--skip-pytest", - help="Run pytest and collect coverage", - dest="run_pytest", - action="store_false", - ) - driver.add_argument( - "--bin-llvm", help="Path to folder where llvm-cov can be found" - ) - driver.add_argument( - "--verbose", - help="Build using vebose makefile mode", - dest="verbose", - action="store_true", - ) - driver.add_argument( - "--gtest-config", - help="Path to the GTestConfig.cmake file to locate a " - + "custom GTest installation.", - ) - args = parser.parse_args() - - if args.oneapi: - args.c_compiler = "icx" - args.cxx_compiler = "icpx" - args.compiler_root = None - icx_path = subprocess.check_output(["which", "icx"]) - bin_dir = os.path.dirname(icx_path) - compiler_dir = os.path.join(bin_dir.decode("utf-8"), "compiler") - if os.path.exists(compiler_dir): - args.bin_llvm = os.path.join(bin_dir.decode("utf-8"), "compiler") - else: - bin_dir = os.path.dirname(bin_dir) - args.bin_llvm = os.path.join(bin_dir.decode("utf-8"), "bin-llvm") - assert os.path.exists(args.bin_llvm) - else: - args_to_validate = [ - "c_compiler", - "cxx_compiler", - "compiler_root", - "bin_llvm", - ] - for p in args_to_validate: - arg = getattr(args, p, None) - if not isinstance(arg, str): - opt_name = p.replace("_", "-") - raise RuntimeError( - f"Option {opt_name} must be provided is " - "using non-default DPC++ layout" - ) - if not os.path.exists(arg): - raise RuntimeError(f"Path {arg} must exist") - run( - use_oneapi=args.oneapi, - c_compiler=args.c_compiler, - cxx_compiler=args.cxx_compiler, - level_zero=args.level_zero, - compiler_root=args.compiler_root, - run_pytest=args.run_pytest, - bin_llvm=args.bin_llvm, - gtest_config=args.gtest_config, - verbose=args.verbose, - ) +if __name__ == "__main__": + main() diff --git a/scripts/gen_docs.py b/scripts/gen_docs.py index b5cf1b0602..610b6ea325 100644 --- a/scripts/gen_docs.py +++ b/scripts/gen_docs.py @@ -14,175 +14,188 @@ # See the License for the specific language governing permissions and # limitations under the License. +import argparse import os import subprocess import sys +# add scripts dir to Python path so we can import _build_helper +sys.path.insert(0, os.path.abspath("scripts")) -def run( - use_oneapi=True, - c_compiler=None, - cxx_compiler=None, - level_zero=True, - compiler_root=None, - bin_llvm=None, - doxyrest_dir=None, - verbose=False, - cmake_opts="", -): - IS_LIN = False - - if "linux" in sys.platform: - IS_LIN = True - elif sys.platform in ["win32", "cygwin"]: - pass - else: - assert False, sys.platform + " not supported" - - if not IS_LIN: - raise RuntimeError( - "This scripts only supports coverage collection on Linux" - ) - setup_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - cmake_args = [ - sys.executable, - "setup.py", - "develop", - "--build-type=Release", - "--generator=Ninja", - "--", - "-DCMAKE_C_COMPILER:PATH=" + c_compiler, - "-DCMAKE_CXX_COMPILER:PATH=" + cxx_compiler, - "-DDPCTL_ENABLE_L0_PROGRAM_CREATION=" + ("ON" if level_zero else "OFF"), - "-DDPCTL_GENERATE_DOCS=ON", - ] - - if verbose: - cmake_args.append("-DCMAKE_VERBOSE_MAKEFILE=ON") - - if doxyrest_dir: - cmake_args.append("-DDPCTL_ENABLE_DOXYREST=ON") - cmake_args.append("-DDoxyrest_DIR=" + doxyrest_dir) - - if cmake_opts: - cmake_args += cmake_opts.split() - - env = dict() - if bin_llvm: - env = { - "PATH": ":".join((os.environ.get("PATH", ""), bin_llvm)), - } - env.update({k: v for k, v in os.environ.items() if k != "PATH"}) - # Install dpctl package - subprocess.check_call(cmake_args, shell=False, cwd=setup_dir, env=env) - # Get the path for the build directory - build_dir = ( - subprocess.check_output( - ["find", "_skbuild", "-name", "cmake-build"], - cwd=setup_dir, - ) - .decode("utf-8") - .strip("\n") - ) - # Generate docs - subprocess.check_call( - ["cmake", "--build", ".", "--target", "Sphinx"], cwd=build_dir - ) - generated_doc_dir = ( - subprocess.check_output( - ["find", "_skbuild", "-name", "index.html"], cwd=setup_dir - ) - .decode("utf-8") - .strip("\n") - ) - print("Generated documentation placed under ", generated_doc_dir) +from _build_helper import ( # noqa: E402 + build_extension, + clean_build_dir, + err, + get_output, + install_editable, + make_cmake_args, + resolve_compilers, + run, + warn, +) -if __name__ == "__main__": - import argparse +def parse_args(): + p = argparse.ArgumentParser(description="Build dpctl and generate coverage") - parser = argparse.ArgumentParser( - description="Driver to build dpctl and generate coverage" + p.add_argument( + "--c-compiler", default=None, help="Path or name of C compiler" + ) + p.add_argument( + "--cxx-compiler", default=None, help="Path or name of C++ compiler" ) - driver = parser.add_argument_group(title="Coverage driver arguments") - driver.add_argument("--c-compiler", help="Name of C compiler", default=None) - driver.add_argument( - "--cxx-compiler", help="Name of C++ compiler", default=None + p.add_argument( + "--compiler-root", + type=str, + default=None, + help="Path to compiler installation root", ) - driver.add_argument( - "--not-oneapi", - help="Is one-API installation", + p.add_argument( + "--oneapi", dest="oneapi", - action="store_false", + action="store_true", + help="Use default oneAPI compiler layout", ) - driver.add_argument( - "--compiler-root", type=str, help="Path to compiler home directory" + + p.add_argument( + "--verbose", + dest="verbose", + action="store_true", + help="Enable verbose makefile output", ) - driver.add_argument( + + p.add_argument( "--no-level-zero", - help="Enable Level Zero support", - dest="level_zero", - action="store_false", + dest="no_level_zero", + action="store_true", + default=False, + help="Disable Level Zero backend (deprecated: use --target-level-zero " + "OFF)", + ) + p.add_argument( + "--target-level-zero", + action="store_true", + help="Enable Level Zero backend explicitly", ) - driver.add_argument( - "--bin-llvm", help="Path to folder where llvm-cov can be found" + + p.add_argument( + "--generator", type=str, default="Ninja", help="CMake generator" + ) + p.add_argument( + "--cmake-executable", + type=str, + default=None, + help="Path to CMake executable used by build", + ) + + p.add_argument( + "--cmake-opts", + type=str, + default="", + help="Additional options to pass directly to CMake", ) - driver.add_argument( + + p.add_argument( "--doxyrest-root", help=( "Path to Doxyrest installation to use to generate Sphinx docs" + "for libsyclinterface" ), ) - driver.add_argument( - "--verbose", - help="Build using vebose makefile mode", - dest="verbose", + + p.add_argument( + "--clean", action="store_true", + help="Remove build dir before rebuild (default: False)", ) - driver.add_argument( - "--cmake-opts", - help="Options to pass through to cmake", - dest="cmake_opts", - default="", - type=str, + + return p.parse_args() + + +def main(): + is_linux = "linux" in sys.platform + if not is_linux: + err(f"{sys.platform} not supported", "gen_docs") + args = parse_args() + setup_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + c_compiler, cxx_compiler = resolve_compilers( + args.oneapi, + args.c_compiler, + args.cxx_compiler, + args.compiler_root, ) - args = parser.parse_args() + if args.clean: + clean_build_dir(setup_dir) - if args.oneapi: - args.c_compiler = "icx" - args.cxx_compiler = "icpx" - args.compiler_root = None - icx_path = subprocess.check_output(["which", "icx"]) - bin_dir = os.path.dirname(icx_path) - args.bin_llvm = os.path.join(bin_dir.decode("utf-8"), "compiler") + if args.no_level_zero and args.target_level_zero: + err( + "Cannot combine --no-level-zero and --target-level-zero", + "gen_coverage", + ) + + # Level Zero state (on unless explicitly disabled) + if args.no_level_zero: + level_zero_enabled = False + elif args.target_level_zero: + level_zero_enabled = True else: - args_to_validate = [ - "c_compiler", - "cxx_compiler", - "compiler_root", - "bin_llvm", - ] - for p in args_to_validate: - arg = getattr(args, p, None) - if not isinstance(arg, str): - opt_name = p.replace("_", "-") - raise RuntimeError( - f"Option {opt_name} must be provided is " - "using non-default DPC++ layout" - ) - if not os.path.exists(arg): - raise RuntimeError(f"Path {arg} must exist") + level_zero_enabled = True - run( - use_oneapi=args.oneapi, - c_compiler=args.c_compiler, - cxx_compiler=args.cxx_compiler, - level_zero=args.level_zero, - compiler_root=args.compiler_root, - bin_llvm=args.bin_llvm, - doxyrest_dir=args.doxyrest_root, + cmake_args = make_cmake_args( + c_compiler=c_compiler, + cxx_compiler=cxx_compiler, + level_zero=level_zero_enabled, verbose=args.verbose, - cmake_opts=args.cmake_opts, ) + + cmake_args += " -DDPCTL_GENERATE_DOCS=ON" + + if args.doxyrest_root: + cmake_args += " -DDPCTL_ENABLE_DOXYREST=ON" + cmake_args += f" -DDoxyrest_DIR={args.doxyrest_root}" + + env = os.environ.copy() + + if "CMAKE_ARGS" in env and env["CMAKE_ARGS"].strip(): + warn("Ignoring pre-existing CMAKE_ARGS in environment", "gen_docs") + del env["CMAKE_ARGS"] + + env["CMAKE_ARGS"] = cmake_args + + print(f"[gen_docs] Using CMake args:\n {env['CMAKE_ARGS']}") + + build_extension( + setup_dir, + env, + cmake_executable=args.cmake_executable, + generator=args.generator, + build_type="Release", + ) + install_editable(setup_dir, env) + cmake_build_dir = get_output( + ["find", "_skbuild", "-name", "cmake-build"], cwd=setup_dir + ) + + print(f"[gen_docs] Found CMake build dir: {cmake_build_dir}") + + run( + ["cmake", "--build", ".", "--target", "Sphinx"], + cwd=cmake_build_dir, + ) + + generated_doc_dir = ( + subprocess.check_output( + ["find", "_skbuild", "-name", "index.html"], cwd=setup_dir + ) + .decode("utf-8") + .strip("\n") + ) + print("Generated documentation placed under ", generated_doc_dir) + + print("[gen_docs] Done") + + +if __name__ == "__main__": + main()