From f78ef7ddb29bf30ff12fbca37d0ff47e1befc1a6 Mon Sep 17 00:00:00 2001 From: "M. Chornyi" <99709299+mc-nv@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:09:56 -0700 Subject: [PATCH 1/4] feat(build): experimental per-backend build presets (--build-presets-file) Add an experimental --build-presets-file flag (gated behind TRITON_BUILD_EXPERIMENTAL=1) that loads a JSON file overriding per-backend properties: git tag, clone organization, library path, and extra/override CMake args. Command-line flags take precedence over the file. Unlike the global --github-organization, a per-backend "org" drives both the git clone URL and -DTRITON_REPO_ORGANIZATION. On --dryrun, the fully-resolved presets (defaults + CLI + file) are written to build_presets.json in the build directory for inspection and reuse. All load/validate/precedence logic lives in tools/build/build_presets.py to keep build.py's integration minimal. No behavioral change when the flag is unused. Refs TRI-829. --- build.py | 64 +++- docs/customization_guide/build.md | 39 +++ tools/build/build_presets.example.json | 20 ++ tools/build/build_presets.py | 406 +++++++++++++++++++++++++ 4 files changed, 528 insertions(+), 1 deletion(-) create mode 100644 tools/build/build_presets.example.json create mode 100644 tools/build/build_presets.py diff --git a/build.py b/build.py index b6aee9cac3..1e4274bd36 100755 --- a/build.py +++ b/build.py @@ -2556,6 +2556,18 @@ def enable_all(): required=False, help="Override specified backend CMake argument in the build as :=. The argument is passed to CMake as -D=. This flag only impacts CMake arguments that are used by build.py. To unconditionally add a CMake argument to the backend build use --extra-backend-cmake-arg.", ) + parser.add_argument( + "--build-presets-file", + type=str, + required=False, + default=None, + help="(EXPERIMENTAL; requires TRITON_BUILD_EXPERIMENTAL=1) Path to a build " + "preset JSON file that overrides per-backend properties (tag, org, " + "library_path, extra_cmake_args, override_cmake_args). Command-line flags " + "take precedence over the file. With --dryrun, the fully-resolved preset " + "(defaults + CLI options + this file) is written to the build directory. " + "See tools/build/build_presets.py for the schema.", + ) parser.add_argument( "--release-version", required=False, @@ -2870,6 +2882,32 @@ def enable_all(): OVERRIDE_BACKEND_CMAKE_FLAGS[be] = {} OVERRIDE_BACKEND_CMAKE_FLAGS[be][parts[0]] = parts[1] + # Per-backend clone-organization overrides. Empty unless the experimental + # --build-presets-file file supplies an "org" for a backend, in which case the + # backend build loop below clones that backend from the given organization. + backend_org_overrides = {} + + # Experimental: apply per-backend overrides from a user-supplied build preset + # JSON file. All load/validation/precedence logic lives in + # tools/build/build_presets.py so this integration stays minimal. + # Command-line flags win over the file. + if FLAGS.build_presets_file is not None: + from tools.build import build_presets + + try: + for msg in build_presets.apply( + FLAGS.build_presets_file, + backends=backends, + cli_backend_specs=FLAGS.backend, + library_paths=library_paths, + extra_flags=EXTRA_BACKEND_CMAKE_FLAGS, + override_flags=OVERRIDE_BACKEND_CMAKE_FLAGS, + backend_org_overrides=backend_org_overrides, + ): + log(msg) + except build_presets.BuildPresetError as e: + fail(str(e)) + # Initialize map of common components and repo-tag for each. components = { "common": default_repo_tag, @@ -2907,6 +2945,28 @@ def enable_all(): # Write the build script that invokes cmake for the core, backends, repo-agents, and caches. pathlib.Path(FLAGS.build_dir).mkdir(parents=True, exist_ok=True) + + # Experimental: on a --dryrun, emit the fully-resolved build preset (defaults + # + CLI options + any --build-presets-file overrides) to the build directory so the + # effective per-backend configuration can be inspected and reused as a + # --build-presets-file input. Gated behind TRITON_BUILD_EXPERIMENTAL=1. + if FLAGS.dryrun and os.getenv("TRITON_BUILD_EXPERIMENTAL") == "1": + from tools.build import build_presets + + try: + for msg in build_presets.dump( + os.path.join(FLAGS.build_dir, "build_presets.json"), + backends=backends, + library_paths=library_paths, + extra_flags=EXTRA_BACKEND_CMAKE_FLAGS, + override_flags=OVERRIDE_BACKEND_CMAKE_FLAGS, + backend_org_overrides=backend_org_overrides, + default_org=FLAGS.github_organization, + ): + log(msg) + except build_presets.BuildPresetError as e: + fail(str(e)) + with BuildScript( os.path.join(FLAGS.build_dir, script_name), verbose=FLAGS.verbose, @@ -2940,7 +3000,9 @@ def enable_all(): if be == "armnn_tflite": github_organization = "https://gitlab.com/arm-research/smarter/" else: - github_organization = FLAGS.github_organization + github_organization = backend_org_overrides.get( + be, FLAGS.github_organization + ) if be == "vllm": backend_clone( diff --git a/docs/customization_guide/build.md b/docs/customization_guide/build.md index 48495c7211..e86cf37f6b 100644 --- a/docs/customization_guide/build.md +++ b/docs/customization_guide/build.md @@ -180,6 +180,45 @@ you have a branch called "mybranch" in the repo that you want to use in the build, you would specify --backend=onnxruntime:mybranch. +#### Experimental: Per-Backend Build Presets + +> **Experimental.** This feature is gated behind the +> `TRITON_BUILD_EXPERIMENTAL=1` environment variable; without it the +> `--build-presets-file` flag is rejected. + +Instead of repeating several per-backend command-line flags, you can supply a +single JSON *build presets* file with `--build-presets-file `. It can +override, per backend, the git tag, the clone organization, the library path, +and extra/override CMake arguments: + +```json +{ + "backends": { + "onnxruntime": { + "tag": "r25.08_fix", + "org": "https://github.com/triton-inference-server", + "override_cmake_args": { "TRITON_ENABLE_ONNXRUNTIME_OPENVINO": "OFF" } + }, + "python": { + "extra_cmake_args": { "TRITON_BOOST_URL": "https://.../boost_1_80_0.tar.gz" } + } + } +} +``` + +A backend named in the file must also be included in the build (via `--backend` +or `--enable-all`). Command-line flags always take precedence over the file, so +the file acts as a reusable base. Unlike the global `--github-organization` +flag, `org` sets the clone organization *per backend* (both the `git clone` URL +and `-DTRITON_REPO_ORGANIZATION`). A ready-to-copy example lives at +[`tools/build/build_presets.example.json`](../../tools/build/build_presets.example.json); +the full schema is documented in `tools/build/build_presets.py`. + +When run with `--dryrun`, build.py also writes the fully-resolved presets +(defaults + command-line options + this file) to `build_presets.json` in the +build directory, which you can inspect and reuse as a `--build-presets-file` +input. + #### CPU-Only Build If you want to build without GPU support you must specify individual diff --git a/tools/build/build_presets.example.json b/tools/build/build_presets.example.json new file mode 100644 index 0000000000..f7c1ac9539 --- /dev/null +++ b/tools/build/build_presets.example.json @@ -0,0 +1,20 @@ +{ + "backends": { + "onnxruntime": { + "tag": "r25.08_fix", + "org": "https://github.com/triton-inference-server", + "override_cmake_args": { + "TRITON_ENABLE_ONNXRUNTIME_OPENVINO": "OFF" + } + }, + "pytorch": { + "tag": "protect_thread_set", + "org": "https://github.com/triton-inference-server" + }, + "python": { + "extra_cmake_args": { + "TRITON_BOOST_URL": "https://archives.boost.io/release/1.80.0/source/boost_1_80_0.tar.gz" + } + } + } +} diff --git a/tools/build/build_presets.py b/tools/build/build_presets.py new file mode 100644 index 0000000000..c0ff863975 --- /dev/null +++ b/tools/build/build_presets.py @@ -0,0 +1,406 @@ +# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Experimental per-backend build configuration overrides for build.py. + +This supporting module lets a user feed build.py a single JSON file that +overrides, *per backend*, any of the following properties: + + { + "backends": { + "": { + "tag": "", # git clone -b + "org": "", # clone URL + -DTRITON_REPO_ORGANIZATION + "library_path": "", # --library-paths equivalent + "extra_cmake_args": { "NAME": "VALUE", ... }, + "override_cmake_args": { "NAME": "VALUE", ... } + } + } + } + +All keys are optional. The feature is experimental and gated behind the +TRITON_BUILD_EXPERIMENTAL=1 environment variable. Command-line flags always take +precedence over values from the file. + +The module keeps *all* load/validate/precedence logic here so that build.py needs +only a thin, minimal integration: it hands its own mutable structures to apply() +and logs the returned messages. +""" + +import json +import os + +ENV_GATE = "TRITON_BUILD_EXPERIMENTAL" + +_ALLOWED_BACKEND_KEYS = frozenset( + ("tag", "org", "library_path", "extra_cmake_args", "override_cmake_args") +) +_STR_KEYS = ("tag", "org", "library_path") +_CMAKE_MAP_KEYS = ("extra_cmake_args", "override_cmake_args") +_REPO_ORG_ARG = "TRITON_REPO_ORGANIZATION" + +# Backends whose clone organization is hardcoded in build.py (not derived from +# --github-organization or a preset "org"). A preset "org" for these would change +# the -DTRITON_REPO_ORGANIZATION arg but NOT the clone URL, which is misleading, +# so it is rejected. +_FIXED_ORG_BACKENDS = frozenset(("armnn_tflite",)) + +# String preset values (tag, org, library_path, cmake values) are interpolated +# unquoted into the generated build shell script, so reject characters that would +# let a preset file inject shell syntax. None of git refs, org URLs, or normal +# paths legitimately contain these. +_SHELL_UNSAFE = frozenset(";|&$`<>\n\r") + + +class BuildPresetError(Exception): + """Raised for any problem loading, validating, or applying a build preset. + + build.py catches this and reports it via its own fail() so the user sees a + clean 'error: ...' message instead of a traceback. + """ + + +def _reject_unsafe(be, key, value): + # Shared string-hygiene check for tag / org / library_path / cmake values. + bad = _SHELL_UNSAFE.intersection(value) + if bad: + raise BuildPresetError( + "--build-presets-file: backend '{}' {} contains unsupported character(s) " + "{}".format(be, key, "".join(sorted(bad))) + ) + + +def _coerce_cmake_value(val): + # JSON booleans map to CMake ON/OFF; everything else stringifies. None is + # rejected earlier in validation (str(None) == "None" is truthy in CMake). + if isinstance(val, bool): + return "ON" if val else "OFF" + return str(val) + + +def _explicit_cli_tags(cli_backend_specs): + # A --backend spec of the form ":" means the user set the tag + # explicitly on the command line; such tags win over the JSON file. An empty + # tag ("--backend be:") is treated as unset, so a preset tag may fill it. + explicit = set() + for spec in cli_backend_specs or []: + parts = spec.split(":", 1) + if len(parts) == 2 and parts[1] != "": + explicit.add(parts[0]) + return explicit + + +def _load(path): + try: + with open(path) as f: + return json.load(f) + except OSError as e: + raise BuildPresetError( + "--build-presets-file: cannot read '{}': {}".format(path, e) + ) + except json.JSONDecodeError as e: + raise BuildPresetError( + "--build-presets-file: invalid JSON in '{}': {}".format(path, e) + ) + + +def _validate(data, build_backends): + if not isinstance(data, dict): + raise BuildPresetError("--build-presets-file: top-level JSON must be an object") + + unknown_top = set(data.keys()) - {"backends"} + if unknown_top: + raise BuildPresetError( + "--build-presets-file: unknown top-level key(s): {}".format( + ", ".join(sorted(unknown_top)) + ) + ) + + if "backends" not in data: + raise BuildPresetError( + "--build-presets-file: missing required top-level key 'backends'" + ) + backends = data["backends"] + if not isinstance(backends, dict): + raise BuildPresetError( + "--build-presets-file: 'backends' must be an object mapping backend name " + "-> properties" + ) + + for be, props in backends.items(): + if be not in build_backends: + raise BuildPresetError( + "--build-presets-file: backend '{}' is not included in the build " + "(add it with --backend {})".format(be, be) + ) + if not isinstance(props, dict): + raise BuildPresetError( + "--build-presets-file: backend '{}' properties must be an object".format( + be + ) + ) + unknown = set(props.keys()) - _ALLOWED_BACKEND_KEYS + if unknown: + raise BuildPresetError( + "--build-presets-file: backend '{}' has unknown key(s): {} " + "(allowed: {})".format( + be, + ", ".join(sorted(unknown)), + ", ".join(sorted(_ALLOWED_BACKEND_KEYS)), + ) + ) + for key in _STR_KEYS: + if key not in props: + continue + value = props[key] + if not isinstance(value, str): + raise BuildPresetError( + "--build-presets-file: backend '{}' key '{}' must be a string".format( + be, key + ) + ) + if value.strip() == "": + raise BuildPresetError( + "--build-presets-file: backend '{}' key '{}' must not be empty".format( + be, key + ) + ) + _reject_unsafe(be, key, value) + for key in _CMAKE_MAP_KEYS: + if key not in props: + continue + argmap = props[key] + if not isinstance(argmap, dict): + raise BuildPresetError( + "--build-presets-file: backend '{}' key '{}' must be an object of " + "CMake name -> value".format(be, key) + ) + for name, arg in argmap.items(): + if arg is None or isinstance(arg, (dict, list)): + raise BuildPresetError( + "--build-presets-file: backend '{}' {}['{}'] must be a non-null " + "scalar value".format(be, key, name) + ) + _reject_unsafe( + be, "{}['{}']".format(key, name), _coerce_cmake_value(arg) + ) + + # "org" and an explicit override of TRITON_REPO_ORGANIZATION describe the + # same thing; requiring both to agree would be surprising, so reject the + # ambiguous combination outright. + if "org" in props and _REPO_ORG_ARG in props.get("override_cmake_args", {}): + raise BuildPresetError( + "--build-presets-file: backend '{}' sets both 'org' and " + "override_cmake_args['{}']; specify only one".format(be, _REPO_ORG_ARG) + ) + + # A preset "org" for a fixed-org backend can't change its clone URL. + if "org" in props and be in _FIXED_ORG_BACKENDS: + raise BuildPresetError( + "--build-presets-file: backend '{}' clone organization is fixed by " + "build.py and cannot be overridden via 'org'".format(be) + ) + + return backends + + +def _apply_cmake_map(be, argmap, flags, label, messages): + if not argmap: + return + bucket = flags.setdefault(be, {}) + for name, val in argmap.items(): + if name in bucket: + messages.append( + " backend '{}': {} cmake '{}' from file ignored (CLI " + "wins)".format(be, label, name) + ) + continue + sval = _coerce_cmake_value(val) + bucket[name] = sval + messages.append( + " backend '{}': {} cmake -D{}={}".format(be, label, name, sval) + ) + + +def apply( + path, + *, + backends, + cli_backend_specs, + library_paths, + extra_flags, + override_flags, + backend_org_overrides, +): + """Load, validate, and apply per-backend overrides from the JSON file at + ``path`` into the passed build.py structures (all mutated in place). + + Args: + path: path to the JSON config file. + backends: build.py's ``{backend: tag}`` map. + cli_backend_specs: the raw ``FLAGS.backend`` list (used to detect which + tags were set explicitly on the command line). + library_paths: build.py's ``{backend: path}`` map. + extra_flags / override_flags: build.py's EXTRA_/OVERRIDE_BACKEND_CMAKE_FLAGS + (``{backend: {name: value}}``). + backend_org_overrides: ``{backend: org_url}`` map that build.py consults + when choosing the git clone organization for each backend. + + Command-line values take precedence over the file. Returns a list of + human-readable log lines. Raises BuildPresetError on any problem. + """ + if os.getenv(ENV_GATE) != "1": + raise BuildPresetError( + "--build-presets-file is experimental; set {}=1 to enable it".format( + ENV_GATE + ) + ) + + config = _validate(_load(path), backends) + explicit_tags = _explicit_cli_tags(cli_backend_specs) + + messages = ["applying experimental --build-presets-file from '{}'".format(path)] + + for be, props in config.items(): + # tag: an explicit CLI "--backend be:tag" wins over the file. + if "tag" in props: + if be in explicit_tags: + messages.append( + " backend '{}': tag '{}' from file ignored (CLI tag " + "wins)".format(be, props["tag"]) + ) + else: + backends[be] = props["tag"] + messages.append(" backend '{}': tag -> {}".format(be, props["tag"])) + + # org: drive BOTH the clone URL (backend_org_overrides) and the + # -DTRITON_REPO_ORGANIZATION arg (via the existing override map). If the + # CLI already pinned the org override, it wins for both. + if "org" in props: + cli_org = override_flags.get(be, {}).get(_REPO_ORG_ARG) + if cli_org is not None: + # CLI already pinned -DTRITON_REPO_ORGANIZATION for this backend. + # Honor "CLI wins" by treating the file "org" as absent: the -D + # keeps the CLI value and the clone org stays the global + # --github-organization, exactly as it would without the preset. + messages.append( + " backend '{}': org '{}' from file ignored (CLI set " + "-D{} explicitly)".format(be, props["org"], _REPO_ORG_ARG) + ) + else: + org = props["org"] + backend_org_overrides[be] = org + override_flags.setdefault(be, {})[_REPO_ORG_ARG] = org + messages.append(" backend '{}': org -> {}".format(be, org)) + + # library_path: a CLI-populated entry wins. + if "library_path" in props: + if be in library_paths: + messages.append( + " backend '{}': library_path from file ignored (CLI " + "wins)".format(be) + ) + else: + library_paths[be] = props["library_path"] + messages.append( + " backend '{}': library_path -> {}".format( + be, props["library_path"] + ) + ) + + # extra / override cmake args: per-arg CLI-wins via setdefault semantics. + _apply_cmake_map( + be, props.get("extra_cmake_args"), extra_flags, "extra", messages + ) + _apply_cmake_map( + be, props.get("override_cmake_args"), override_flags, "override", messages + ) + + return messages + + +def dump( + path, + *, + backends, + library_paths, + extra_flags, + override_flags, + backend_org_overrides, + default_org, +): + """Serialize the fully-resolved per-backend build configuration to ``path`` + as a build preset JSON. + + The output captures the effective values after defaults, command-line + options, and any applied --build-presets-file overrides have been merged, and is a + valid --build-presets-file input itself (round-trippable). Args mirror the build.py + structures passed to apply(); ``default_org`` is FLAGS.github_organization, + used for any backend without a per-backend org override. + + Returns a list of human-readable log lines. Raises BuildPresetError if the + file cannot be written. + """ + out = {"backends": {}} + for be in sorted(backends): + entry = {"tag": backends[be]} + if be in library_paths: + entry["library_path"] = library_paths[be] + extra = extra_flags.get(be) + if extra: + entry["extra_cmake_args"] = dict(extra) + + # Represent the clone organization faithfully and round-trippably. + # eff_org is the effective clone org; d is any -DTRITON_REPO_ORGANIZATION. + eff_org = backend_org_overrides.get(be, default_org) + override = dict(override_flags.get(be) or {}) + d = override.get(_REPO_ORG_ARG) + if d is not None and d == eff_org: + # Coupled (clone org == -D): express via "org" and drop the redundant + # -D so reload does not trip the org/override conflict check. + entry["org"] = eff_org + del override[_REPO_ORG_ARG] + elif d is not None: + # A -D override that differs from the (default) clone org: keep it as + # an override and omit "org" (clone org stays the global default). + pass + else: + entry["org"] = eff_org + + if override: + entry["override_cmake_args"] = override + out["backends"][be] = entry + + try: + with open(path, "w") as f: + json.dump(out, f, indent=2, sort_keys=True) + f.write("\n") + except OSError as e: + raise BuildPresetError( + "--build-presets-file dump: cannot write '{}': {}".format(path, e) + ) + + return ["wrote resolved build preset to '{}'".format(path)] From 811170e988acf4aee0989da76a60c1542b45043c Mon Sep 17 00:00:00 2001 From: "M. Chornyi" <99709299+mc-nv@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:51:00 -0700 Subject: [PATCH 2/4] feat(build): make build presets a full provenance-annotated, reloadable snapshot Rework the experimental --build-presets-file dump/reload: - dump: on --dryrun, write build_presets.json capturing EVERY resolved cmake -D flag each component receives (core/backends/repoagents/caches), matching cmake_build, each annotated with its source (cli/preset/default). Provenance is derived from argv (which options were passed) plus before/after snapshots of the extra/override dicts. - reload: the same file loads back and pins every flag. cmake_args route to the override channel (flags build.py emits natively); extra_cmake_args route to the extra/append channel (user-added flags such as TRITON_BOOST_URL) so they are not dropped. TRITON_REPO_ORGANIZATION in a backend's cmake_args also drives the per-backend clone org. CLI flags still win. - values may be a scalar or a {value, source} object; CMAKE_INSTALL_PREFIX is excluded (absolute path). Repoagent/cache cmake_args are visibility-only on load (tag is pinned). All snapshot/parse/annotate logic lives in tools/build/build_presets.py (write_snapshot); build.py only gathers the per-component cmake args and calls it. No behavioral change when the flag is unused (CI variants byte-identical). Refs TRI-829. --- build.py | 92 ++- docs/customization_guide/build.md | 64 ++- tools/build/build_presets.example.json | 7 +- tools/build/build_presets.py | 738 ++++++++++++++++--------- 4 files changed, 604 insertions(+), 297 deletions(-) diff --git a/build.py b/build.py index 1e4274bd36..a497a527cf 100755 --- a/build.py +++ b/build.py @@ -2562,11 +2562,13 @@ def enable_all(): required=False, default=None, help="(EXPERIMENTAL; requires TRITON_BUILD_EXPERIMENTAL=1) Path to a build " - "preset JSON file that overrides per-backend properties (tag, org, " - "library_path, extra_cmake_args, override_cmake_args). Command-line flags " - "take precedence over the file. With --dryrun, the fully-resolved preset " - "(defaults + CLI options + this file) is written to the build directory. " - "See tools/build/build_presets.py for the schema.", + "presets JSON file that pins per-component cmake flags (tag, cmake_args, " + "extra_cmake_args, library_path) for core/backends/repoagents/caches. " + "Command-line flags take precedence over the file. With --dryrun, a " + "provenance-annotated snapshot of every resolved cmake flag (labeled " + "cli/preset/default) is written to build_presets.json in the build " + "directory; that file can be loaded back with this flag. See " + "tools/build/build_presets.py for the schema.", ) parser.add_argument( "--release-version", @@ -2883,14 +2885,21 @@ def enable_all(): OVERRIDE_BACKEND_CMAKE_FLAGS[be][parts[0]] = parts[1] # Per-backend clone-organization overrides. Empty unless the experimental - # --build-presets-file file supplies an "org" for a backend, in which case the - # backend build loop below clones that backend from the given organization. + # --build-presets-file supplies a TRITON_REPO_ORGANIZATION for a backend, in + # which case the backend build loop below clones from that organization. backend_org_overrides = {} - # Experimental: apply per-backend overrides from a user-supplied build preset - # JSON file. All load/validation/precedence logic lives in - # tools/build/build_presets.py so this integration stays minimal. - # Command-line flags win over the file. + # Snapshots captured BEFORE applying a preset, so the --dryrun dump can tell + # which values came from the CLI vs the preset file vs build.py defaults. + _bp_before_backends = dict(backends) + _bp_before_repoagents = dict(repoagents) + _bp_before_caches = dict(caches) + _bp_cli_extra_be = {be: set(m) for be, m in EXTRA_BACKEND_CMAKE_FLAGS.items()} + _bp_cli_override_be = {be: set(m) for be, m in OVERRIDE_BACKEND_CMAKE_FLAGS.items()} + _bp_cli_core = set(EXTRA_CORE_CMAKE_FLAGS) | set(OVERRIDE_CORE_CMAKE_FLAGS) + + # Experimental: apply a build preset (all load/validate/precedence logic lives + # in tools/build/build_presets.py). Command-line flags win over the file. if FLAGS.build_presets_file is not None: from tools.build import build_presets @@ -2898,10 +2907,16 @@ def enable_all(): for msg in build_presets.apply( FLAGS.build_presets_file, backends=backends, + repoagents=repoagents, + caches=caches, cli_backend_specs=FLAGS.backend, + cli_repoagent_specs=FLAGS.repoagent, + cli_cache_specs=FLAGS.cache, library_paths=library_paths, - extra_flags=EXTRA_BACKEND_CMAKE_FLAGS, - override_flags=OVERRIDE_BACKEND_CMAKE_FLAGS, + extra_backend_flags=EXTRA_BACKEND_CMAKE_FLAGS, + override_backend_flags=OVERRIDE_BACKEND_CMAKE_FLAGS, + extra_core_flags=EXTRA_CORE_CMAKE_FLAGS, + override_core_flags=OVERRIDE_CORE_CMAKE_FLAGS, backend_org_overrides=backend_org_overrides, ): log(msg) @@ -2946,22 +2961,55 @@ def enable_all(): # Write the build script that invokes cmake for the core, backends, repo-agents, and caches. pathlib.Path(FLAGS.build_dir).mkdir(parents=True, exist_ok=True) - # Experimental: on a --dryrun, emit the fully-resolved build preset (defaults - # + CLI options + any --build-presets-file overrides) to the build directory so the - # effective per-backend configuration can be inspected and reused as a - # --build-presets-file input. Gated behind TRITON_BUILD_EXPERIMENTAL=1. + # Experimental: on a --dryrun, emit a provenance-annotated snapshot of the + # fully-resolved cmake configuration -- every -D each component receives, as + # in cmake_build -- to /build_presets.json, labeling each value's + # source (cli / preset / default). The file can be loaded back with + # --build-presets-file to pin every flag. Gated behind + # TRITON_BUILD_EXPERIMENTAL=1. if FLAGS.dryrun and os.getenv("TRITON_BUILD_EXPERIMENTAL") == "1": from tools.build import build_presets + _bp_be = [b for b in backends if b not in CORE_BACKENDS] try: - for msg in build_presets.dump( + for msg in build_presets.write_snapshot( os.path.join(FLAGS.build_dir, "build_presets.json"), + parser=parser, + argv=sys.argv, + core_args=core_cmake_args( + components, backends, script_cmake_dir, script_install_dir + ), + backend_args={ + be: backend_cmake_args( + images, components, be, script_install_dir, library_paths + ) + for be in _bp_be + }, + repoagent_args={ + ra: repoagent_cmake_args(images, components, ra, script_install_dir) + for ra in repoagents + }, + cache_args={ + ca: cache_cmake_args(images, components, ca, script_install_dir) + for ca in caches + }, backends=backends, + repoagents=repoagents, + caches=caches, + before_backends=_bp_before_backends, + before_repoagents=_bp_before_repoagents, + before_caches=_bp_before_caches, + cli_backend_specs=FLAGS.backend, + cli_repoagent_specs=FLAGS.repoagent, + cli_cache_specs=FLAGS.cache, + cli_extra_be=_bp_cli_extra_be, + cli_override_be=_bp_cli_override_be, + cli_core=_bp_cli_core, + extra_backend_flags=EXTRA_BACKEND_CMAKE_FLAGS, + override_backend_flags=OVERRIDE_BACKEND_CMAKE_FLAGS, + extra_core_flags=EXTRA_CORE_CMAKE_FLAGS, + override_core_flags=OVERRIDE_CORE_CMAKE_FLAGS, library_paths=library_paths, - extra_flags=EXTRA_BACKEND_CMAKE_FLAGS, - override_flags=OVERRIDE_BACKEND_CMAKE_FLAGS, - backend_org_overrides=backend_org_overrides, - default_org=FLAGS.github_organization, ): log(msg) except build_presets.BuildPresetError as e: diff --git a/docs/customization_guide/build.md b/docs/customization_guide/build.md index e86cf37f6b..b37f579339 100644 --- a/docs/customization_guide/build.md +++ b/docs/customization_guide/build.md @@ -180,24 +180,47 @@ you have a branch called "mybranch" in the repo that you want to use in the build, you would specify --backend=onnxruntime:mybranch. -#### Experimental: Per-Backend Build Presets +#### Experimental: Build Presets > **Experimental.** This feature is gated behind the > `TRITON_BUILD_EXPERIMENTAL=1` environment variable; without it the > `--build-presets-file` flag is rejected. -Instead of repeating several per-backend command-line flags, you can supply a -single JSON *build presets* file with `--build-presets-file `. It can -override, per backend, the git tag, the clone organization, the library path, -and extra/override CMake arguments: +Build presets let you (a) inspect exactly which cmake flags each component +receives, and (b) pin those flags via a single JSON file with +`--build-presets-file `. + +**Snapshot (dump).** When run with `--dryrun`, build.py writes a +provenance-annotated snapshot of the fully-resolved cmake configuration to +`build_presets.json` in the build directory. It records, for `core`, each +`backend`, `repoagent`, and `cache`, every `-D` flag that lands in `cmake_build`, +each labeled with its `source`: `cli` (explicit command line), `preset` (from a +loaded presets file), or `default` (build.py default/derived): + +```json +{ + "backends": { + "onnxruntime": { + "tag": { "value": "main", "source": "default" }, + "cmake_args": { + "TRITON_ENABLE_GPU": { "value": "ON", "source": "cli" }, + "TRITON_BUILD_ONNXRUNTIME_VERSION": { "value": "1.27.0", "source": "default" } + } + } + } +} +``` + +**Reload.** That same file can be fed back with `--build-presets-file` to pin its +flags (the `source` field is informational on load). A hand-written preset may +use bare scalars instead of `{value, source}` objects: ```json { "backends": { "onnxruntime": { "tag": "r25.08_fix", - "org": "https://github.com/triton-inference-server", - "override_cmake_args": { "TRITON_ENABLE_ONNXRUNTIME_OPENVINO": "OFF" } + "cmake_args": { "TRITON_ENABLE_ONNXRUNTIME_OPENVINO": "OFF" } }, "python": { "extra_cmake_args": { "TRITON_BOOST_URL": "https://.../boost_1_80_0.tar.gz" } @@ -206,19 +229,26 @@ and extra/override CMake arguments: } ``` -A backend named in the file must also be included in the build (via `--backend` -or `--enable-all`). Command-line flags always take precedence over the file, so -the file acts as a reusable base. Unlike the global `--github-organization` -flag, `org` sets the clone organization *per backend* (both the `git clone` URL -and `-DTRITON_REPO_ORGANIZATION`). A ready-to-copy example lives at +Notes: +- A component named in the file must also be included in the build (via + `--backend`/`--repoagent`/`--cache` or `--enable-all`). +- Command-line flags always take precedence over the file. +- `cmake_args` are flags build.py emits natively (applied via the override + channel); `extra_cmake_args` are user-added flags build.py does not emit + (applied via the append channel). Setting `TRITON_REPO_ORGANIZATION` in a + backend's `cmake_args` sets that backend's clone organization *per backend* + (both the `git clone` URL and the `-D`), which the global + `--github-organization` cannot do. +- `CMAKE_INSTALL_PREFIX` is omitted from snapshots (it is an absolute build-dir + path). Repoagent/cache `cmake_args` are shown for visibility but only their + `tag` is re-pinned on load. Reloading pins `-D` values but does not re-derive + conditionally-emitted flags, so reload alongside the same top-level flags (or + `--enable-all`) for exact reproduction. + +A ready-to-copy example lives at [`tools/build/build_presets.example.json`](../../tools/build/build_presets.example.json); the full schema is documented in `tools/build/build_presets.py`. -When run with `--dryrun`, build.py also writes the fully-resolved presets -(defaults + command-line options + this file) to `build_presets.json` in the -build directory, which you can inspect and reuse as a `--build-presets-file` -input. - #### CPU-Only Build If you want to build without GPU support you must specify individual diff --git a/tools/build/build_presets.example.json b/tools/build/build_presets.example.json index f7c1ac9539..4dfa5f1e63 100644 --- a/tools/build/build_presets.example.json +++ b/tools/build/build_presets.example.json @@ -2,14 +2,13 @@ "backends": { "onnxruntime": { "tag": "r25.08_fix", - "org": "https://github.com/triton-inference-server", - "override_cmake_args": { + "cmake_args": { + "TRITON_REPO_ORGANIZATION": "https://github.com/triton-inference-server", "TRITON_ENABLE_ONNXRUNTIME_OPENVINO": "OFF" } }, "pytorch": { - "tag": "protect_thread_set", - "org": "https://github.com/triton-inference-server" + "tag": "protect_thread_set" }, "python": { "extra_cmake_args": { diff --git a/tools/build/build_presets.py b/tools/build/build_presets.py index c0ff863975..fe7c87a87d 100644 --- a/tools/build/build_presets.py +++ b/tools/build/build_presets.py @@ -24,30 +24,45 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Experimental per-backend build configuration overrides for build.py. +"""Experimental build presets for build.py. -This supporting module lets a user feed build.py a single JSON file that -overrides, *per backend*, any of the following properties: +This supporting module implements the experimental ``--build-presets-file`` flag, +gated behind the ``TRITON_BUILD_EXPERIMENTAL=1`` environment variable. + +Two directions: + +* ``dump()`` writes a *complete, provenance-annotated snapshot* of the resolved + cmake configuration to ``/build_presets.json`` on a --dryrun. Every + cmake ``-D`` flag each component (core / backends / repoagents / caches) + receives -- exactly what lands in ``cmake_build`` -- is recorded together with + the git tag, each annotated with its ``source``: ``cli`` (explicit command + line), ``preset`` (from a loaded presets file), or ``default`` (build.py + default/derived). + +* ``apply()`` loads that same file back and *pins every flag* so the build is + reproduced exactly. The ``source`` field is informational on load. Explicit + command-line flags still win over the file. + +Schema (produced by dump(), accepted by apply()):: { - "backends": { - "": { - "tag": "", # git clone -b - "org": "", # clone URL + -DTRITON_REPO_ORGANIZATION - "library_path": "", # --library-paths equivalent - "extra_cmake_args": { "NAME": "VALUE", ... }, - "override_cmake_args": { "NAME": "VALUE", ... } - } - } + "core": { "cmake_args": {...}, "extra_cmake_args": {...} }, + "backends": { "": { "tag": {"value":..,"source":..}, + "cmake_args": {...}, # -> override channel + "extra_cmake_args": {...}, # -> extra (append) channel + "library_path": {...} } }, + "repoagents": { "": { "tag": {...}, "cmake_args": {...} } }, + "caches": { "": { "tag": {...}, "cmake_args": {...} } } } -All keys are optional. The feature is experimental and gated behind the -TRITON_BUILD_EXPERIMENTAL=1 environment variable. Command-line flags always take -precedence over values from the file. - -The module keeps *all* load/validate/precedence logic here so that build.py needs -only a thin, minimal integration: it hands its own mutable structures to apply() -and logs the returned messages. +A cmake value / tag may be a bare scalar or a ``{"value", "source"}`` object; on +load only ``value`` is used. ``cmake_args`` holds flags build.py emits natively +(reloaded via the override channel); ``extra_cmake_args`` holds user-added flags +build.py does not emit (reloaded via the extra/append channel). +``CMAKE_INSTALL_PREFIX`` is omitted from dumps (absolute build-dir path -- must +not be pinned). Repoagent/cache ``cmake_args`` are dumped for visibility but not +re-pinned on load (build.py has no per-repoagent/cache cmake-override channel); +their ``tag`` is applied. """ import json @@ -55,25 +70,62 @@ ENV_GATE = "TRITON_BUILD_EXPERIMENTAL" -_ALLOWED_BACKEND_KEYS = frozenset( - ("tag", "org", "library_path", "extra_cmake_args", "override_cmake_args") -) -_STR_KEYS = ("tag", "org", "library_path") -_CMAKE_MAP_KEYS = ("extra_cmake_args", "override_cmake_args") _REPO_ORG_ARG = "TRITON_REPO_ORGANIZATION" -# Backends whose clone organization is hardcoded in build.py (not derived from -# --github-organization or a preset "org"). A preset "org" for these would change -# the -DTRITON_REPO_ORGANIZATION arg but NOT the clone URL, which is misleading, -# so it is rejected. +# Backends whose clone organization is hardcoded in build.py; a preset cannot +# change their clone URL (the -D is still pinned, harmlessly). _FIXED_ORG_BACKENDS = frozenset(("armnn_tflite",)) -# String preset values (tag, org, library_path, cmake values) are interpolated -# unquoted into the generated build shell script, so reject characters that would -# let a preset file inject shell syntax. None of git refs, org URLs, or normal -# paths legitimately contain these. +# String values are interpolated unquoted into the generated build shell script, +# so reject characters that would let a preset file inject shell syntax. _SHELL_UNSAFE = frozenset(";|&$`<>\n\r") +# Environment/path-specific flags excluded from dumps -- pinning them would break +# reloading the preset into a different build directory. +_DUMP_EXCLUDE = frozenset(("CMAKE_INSTALL_PREFIX",)) + +_SECTION_KEYS = ("core", "backends", "repoagents", "caches") + +# cmake flag name -> the build.py CLI option (argparse dest) that controls it. +# Used only for provenance labeling in dump(); flags not listed are treated as +# build.py-computed defaults/derivations (source "default") unless they came +# through an explicit --extra/override-*-cmake-arg. +_FLAG_TO_DEST = { + "CMAKE_BUILD_TYPE": "build_type", + "TRITON_VERSION": "version", + "TRITON_REPO_ORGANIZATION": "github_organization", + "TRITON_COMMON_REPO_TAG": "repo_tag", + "TRITON_CORE_REPO_TAG": "repo_tag", + "TRITON_BACKEND_REPO_TAG": "repo_tag", + "TRITON_THIRD_PARTY_REPO_TAG": "repo_tag", + "TRITON_ENABLE_LOGGING": "enable_logging", + "TRITON_ENABLE_STATS": "enable_stats", + "TRITON_ENABLE_METRICS": "enable_metrics", + "TRITON_ENABLE_METRICS_GPU": "enable_gpu_metrics", + "TRITON_ENABLE_METRICS_CPU": "enable_cpu_metrics", + "TRITON_ENABLE_TRACING": "enable_tracing", + "TRITON_ENABLE_NVTX": "enable_nvtx", + "TRITON_ENABLE_GPU": "enable_gpu", + "TRITON_MIN_COMPUTE_CAPABILITY": "min_compute_capability", + "TRITON_ENABLE_MALI_GPU": "enable_mali_gpu", + "TRITON_ENABLE_GRPC": "endpoint", + "TRITON_ENABLE_HTTP": "endpoint", + "TRITON_ENABLE_SAGEMAKER": "endpoint", + "TRITON_ENABLE_VERTEX_AI": "endpoint", + "TRITON_ENABLE_GCS": "filesystem", + "TRITON_ENABLE_S3": "filesystem", + "TRITON_ENABLE_AZURE_STORAGE": "filesystem", + "TRITON_ENABLE_ENSEMBLE": "backend", + "TRITON_ENABLE_TENSORRT": "backend", + "PYBIND11_PYTHON_VERSION": "rhel_py_version", + "TRITON_PYTORCH_DOCKER_IMAGE": "image", + "TRITON_BUILD_CONTAINER": "image", + "TRITON_BUILD_ONNXRUNTIME_VERSION": "ort_version", + "TRITON_BUILD_ONNXRUNTIME_OPENVINO_VERSION": "ort_openvino_version", + "TRITON_BUILD_OPENVINO_VERSION": "standalone_openvino_version", + "TRITON_BUILD_CONTAINER_VERSION": "upstream_container_version", +} + class BuildPresetError(Exception): """Raised for any problem loading, validating, or applying a build preset. @@ -83,34 +135,82 @@ class BuildPresetError(Exception): """ -def _reject_unsafe(be, key, value): - # Shared string-hygiene check for tag / org / library_path / cmake values. - bad = _SHELL_UNSAFE.intersection(value) - if bad: - raise BuildPresetError( - "--build-presets-file: backend '{}' {} contains unsupported character(s) " - "{}".format(be, key, "".join(sorted(bad))) - ) +# --------------------------------------------------------------------------- # +# Shared helpers +# --------------------------------------------------------------------------- # +def explicit_cli_tags(cli_specs): + # A ":" spec means the tag was set explicitly on the command line + # (and so wins over the file). An empty tag ("name:") is treated as unset. + explicit = set() + for spec in cli_specs or []: + parts = spec.split(":", 1) + if len(parts) == 2 and parts[1] != "": + explicit.add(parts[0]) + return explicit + + +def provided_options(parser, argv): + """Return the set of argparse dests whose option strings actually appear on + the command line (argv). Lets dump() label a flag's value as coming from the + CLI even when it equals build.py's default.""" + opt_to_dest = {} + for action in parser._actions: + for opt in action.option_strings: + opt_to_dest[opt] = action.dest + provided = set() + for tok in argv[1:] if argv else []: + key = tok.split("=", 1)[0] + if key in opt_to_dest: + provided.add(opt_to_dest[key]) + return provided + + +def parse_cmake_defs(arg_list): + """Parse a list of cmake arguments (as produced by build.py's *_cmake_args + helpers) into an ordered {name: value} dict, dropping non -D entries and the + environment/path-specific excludes.""" + defs = {} + for raw in arg_list: + s = str(raw).strip().strip('"') + if not s.startswith("-D"): + continue + body = s[2:] + if "=" not in body: + continue + lhs, value = body.split("=", 1) + name = lhs.split(":", 1)[0] + if name in _DUMP_EXCLUDE: + continue + defs[name] = value + return defs + + +def flag_source(flag, *, provided_dests, is_cli=False, is_preset=False): + """Classify the provenance of a resolved cmake flag.""" + if is_preset: + return "preset" + if is_cli: + return "cli" + dest = _FLAG_TO_DEST.get(flag) + if dest is not None and dest in provided_dests: + return "cli" + return "default" def _coerce_cmake_value(val): - # JSON booleans map to CMake ON/OFF; everything else stringifies. None is - # rejected earlier in validation (str(None) == "None" is truthy in CMake). + # JSON booleans map to CMake ON/OFF; everything else stringifies. if isinstance(val, bool): return "ON" if val else "OFF" return str(val) -def _explicit_cli_tags(cli_backend_specs): - # A --backend spec of the form ":" means the user set the tag - # explicitly on the command line; such tags win over the JSON file. An empty - # tag ("--backend be:") is treated as unset, so a preset tag may fill it. - explicit = set() - for spec in cli_backend_specs or []: - parts = spec.split(":", 1) - if len(parts) == 2 and parts[1] != "": - explicit.add(parts[0]) - return explicit +def _reject_unsafe(where, value): + bad = _SHELL_UNSAFE.intersection(value) + if bad: + raise BuildPresetError( + "--build-presets-file: {} contains unsupported character(s) " + "{}".format(where, "".join(sorted(bad))) + ) def _load(path): @@ -127,150 +227,186 @@ def _load(path): ) -def _validate(data, build_backends): - if not isinstance(data, dict): - raise BuildPresetError("--build-presets-file: top-level JSON must be an object") +# --------------------------------------------------------------------------- # +# Validation +# --------------------------------------------------------------------------- # +def _read_scalar(raw, where, *, allow_bool_num=False): + """Return the plain value from a scalar or a {"value", "source"} object, + validating it. ``where`` is a human label for error messages.""" + if isinstance(raw, dict): + if "value" not in raw: + raise BuildPresetError( + "--build-presets-file: {} object must contain 'value'".format(where) + ) + unknown = set(raw.keys()) - {"value", "source"} + if unknown: + raise BuildPresetError( + "--build-presets-file: {} has unknown key(s): {}".format( + where, ", ".join(sorted(unknown)) + ) + ) + value = raw["value"] + else: + value = raw - unknown_top = set(data.keys()) - {"backends"} - if unknown_top: + if value is None or isinstance(value, (dict, list)): raise BuildPresetError( - "--build-presets-file: unknown top-level key(s): {}".format( - ", ".join(sorted(unknown_top)) - ) + "--build-presets-file: {} must be a non-null scalar".format(where) ) - - if "backends" not in data: + if not allow_bool_num and not isinstance(value, str): raise BuildPresetError( - "--build-presets-file: missing required top-level key 'backends'" + "--build-presets-file: {} must be a string".format(where) ) - backends = data["backends"] - if not isinstance(backends, dict): + return value + + +def _validate_cmake_args(cmake_args, where): + if not isinstance(cmake_args, dict): raise BuildPresetError( - "--build-presets-file: 'backends' must be an object mapping backend name " - "-> properties" + "--build-presets-file: {} must be an object".format(where) ) + for flag, raw in cmake_args.items(): + value = _read_scalar(raw, "{}['{}']".format(where, flag), allow_bool_num=True) + _reject_unsafe("{}['{}']".format(where, flag), _coerce_cmake_value(value)) - for be, props in backends.items(): - if be not in build_backends: - raise BuildPresetError( - "--build-presets-file: backend '{}' is not included in the build " - "(add it with --backend {})".format(be, be) + +def _validate_component(name, entry, kind, allow_extra): + if not isinstance(entry, dict): + raise BuildPresetError( + "--build-presets-file: {} '{}' must be an object".format(kind, name) + ) + allowed = {"tag", "cmake_args"} + if allow_extra: + allowed |= {"extra_cmake_args", "library_path"} + unknown = set(entry.keys()) - allowed + if unknown: + raise BuildPresetError( + "--build-presets-file: {} '{}' has unknown key(s): {} (allowed: " + "{})".format( + kind, name, ", ".join(sorted(unknown)), ", ".join(sorted(allowed)) ) - if not isinstance(props, dict): + ) + if "tag" in entry: + tag = _read_scalar(entry["tag"], "{} '{}' tag".format(kind, name)) + if tag.strip() == "": raise BuildPresetError( - "--build-presets-file: backend '{}' properties must be an object".format( - be - ) + "--build-presets-file: {} '{}' tag must not be empty".format(kind, name) ) - unknown = set(props.keys()) - _ALLOWED_BACKEND_KEYS - if unknown: - raise BuildPresetError( - "--build-presets-file: backend '{}' has unknown key(s): {} " - "(allowed: {})".format( - be, - ", ".join(sorted(unknown)), - ", ".join(sorted(_ALLOWED_BACKEND_KEYS)), - ) + _reject_unsafe("{} '{}' tag".format(kind, name), tag) + if "library_path" in entry: + lp = _read_scalar( + entry["library_path"], "{} '{}' library_path".format(kind, name) + ) + _reject_unsafe("{} '{}' library_path".format(kind, name), lp) + for key in ("cmake_args", "extra_cmake_args"): + if key in entry: + _validate_cmake_args(entry[key], "{} '{}' {}".format(kind, name, key)) + + +def _validate(data, build_backends, build_repoagents, build_caches): + if not isinstance(data, dict): + raise BuildPresetError("--build-presets-file: top-level JSON must be an object") + + unknown_top = set(data.keys()) - (set(_SECTION_KEYS) | {"_legend"}) + if unknown_top: + raise BuildPresetError( + "--build-presets-file: unknown top-level key(s): {} (allowed: {})".format( + ", ".join(sorted(unknown_top)), + ", ".join(sorted(_SECTION_KEYS) + ["_legend"]), ) - for key in _STR_KEYS: - if key not in props: - continue - value = props[key] - if not isinstance(value, str): - raise BuildPresetError( - "--build-presets-file: backend '{}' key '{}' must be a string".format( - be, key - ) - ) - if value.strip() == "": - raise BuildPresetError( - "--build-presets-file: backend '{}' key '{}' must not be empty".format( - be, key - ) - ) - _reject_unsafe(be, key, value) - for key in _CMAKE_MAP_KEYS: - if key not in props: - continue - argmap = props[key] - if not isinstance(argmap, dict): - raise BuildPresetError( - "--build-presets-file: backend '{}' key '{}' must be an object of " - "CMake name -> value".format(be, key) - ) - for name, arg in argmap.items(): - if arg is None or isinstance(arg, (dict, list)): - raise BuildPresetError( - "--build-presets-file: backend '{}' {}['{}'] must be a non-null " - "scalar value".format(be, key, name) - ) - _reject_unsafe( - be, "{}['{}']".format(key, name), _coerce_cmake_value(arg) - ) + ) - # "org" and an explicit override of TRITON_REPO_ORGANIZATION describe the - # same thing; requiring both to agree would be surprising, so reject the - # ambiguous combination outright. - if "org" in props and _REPO_ORG_ARG in props.get("override_cmake_args", {}): + core = data.get("core") + if core is not None: + if not isinstance(core, dict) or ( + set(core.keys()) - {"cmake_args", "extra_cmake_args"} + ): raise BuildPresetError( - "--build-presets-file: backend '{}' sets both 'org' and " - "override_cmake_args['{}']; specify only one".format(be, _REPO_ORG_ARG) + "--build-presets-file: 'core' must be an object with only " + "'cmake_args' and/or 'extra_cmake_args'" ) - - # A preset "org" for a fixed-org backend can't change its clone URL. - if "org" in props and be in _FIXED_ORG_BACKENDS: + for key in ("cmake_args", "extra_cmake_args"): + if key in core: + _validate_cmake_args(core[key], "core {}".format(key)) + + for section, valid_names, kind, allow_extra in ( + ("backends", build_backends, "backend", True), + ("repoagents", build_repoagents, "repoagent", False), + ("caches", build_caches, "cache", False), + ): + block = data.get(section) + if block is None: + continue + if not isinstance(block, dict): raise BuildPresetError( - "--build-presets-file: backend '{}' clone organization is fixed by " - "build.py and cannot be overridden via 'org'".format(be) + "--build-presets-file: '{}' must be an object".format(section) ) + for name, entry in block.items(): + if name not in valid_names: + raise BuildPresetError( + "--build-presets-file: {} '{}' is not included in the build " + "(add it with --{} {})".format(kind, name, kind, name) + ) + _validate_component(name, entry, kind, allow_extra) - return backends + return data -def _apply_cmake_map(be, argmap, flags, label, messages): - if not argmap: +# --------------------------------------------------------------------------- # +# apply() -- load a preset and pin its values +# --------------------------------------------------------------------------- # +def _apply_tag(name, entry, tags, explicit, kind, messages): + if "tag" not in entry: return - bucket = flags.setdefault(be, {}) - for name, val in argmap.items(): - if name in bucket: + tag = _read_scalar(entry["tag"], "{} '{}' tag".format(kind, name)) + if name in explicit: + messages.append( + " {} '{}': tag '{}' from file ignored (CLI tag wins)".format( + kind, name, tag + ) + ) + else: + tags[name] = tag + messages.append(" {} '{}': tag -> {}".format(kind, name, tag)) + + +def _pin_into(bucket, cli_names, cmake_args, label, messages): + # setdefault semantics: an explicit CLI value for the same flag wins. + for flag, raw in cmake_args.items(): + value = _coerce_cmake_value(_read_scalar(raw, label, allow_bool_num=True)) + if flag in cli_names: messages.append( - " backend '{}': {} cmake '{}' from file ignored (CLI " - "wins)".format(be, label, name) + " {}: cmake '{}' from file ignored (CLI wins)".format(label, flag) ) continue - sval = _coerce_cmake_value(val) - bucket[name] = sval - messages.append( - " backend '{}': {} cmake -D{}={}".format(be, label, name, sval) - ) + bucket[flag] = value + messages.append(" {}: cmake -D{}={}".format(label, flag, value)) def apply( path, *, backends, + repoagents, + caches, cli_backend_specs, + cli_repoagent_specs, + cli_cache_specs, library_paths, - extra_flags, - override_flags, + extra_backend_flags, + override_backend_flags, + extra_core_flags, + override_core_flags, backend_org_overrides, ): - """Load, validate, and apply per-backend overrides from the JSON file at - ``path`` into the passed build.py structures (all mutated in place). - - Args: - path: path to the JSON config file. - backends: build.py's ``{backend: tag}`` map. - cli_backend_specs: the raw ``FLAGS.backend`` list (used to detect which - tags were set explicitly on the command line). - library_paths: build.py's ``{backend: path}`` map. - extra_flags / override_flags: build.py's EXTRA_/OVERRIDE_BACKEND_CMAKE_FLAGS - (``{backend: {name: value}}``). - backend_org_overrides: ``{backend: org_url}`` map that build.py consults - when choosing the git clone organization for each backend. - - Command-line values take precedence over the file. Returns a list of - human-readable log lines. Raises BuildPresetError on any problem. + """Load, validate, and apply a build preset from ``path`` into build.py's + structures (all mutated in place). Every flag in the file is pinned so the + build is reproduced; explicit command-line flags win. + + ``cmake_args`` route to the override channel (flags build.py emits natively); + ``extra_cmake_args`` route to the extra/append channel (user-added flags). + + Returns human-readable log lines. Raises BuildPresetError on any problem. """ if os.getenv(ENV_GATE) != "1": raise BuildPresetError( @@ -279,128 +415,222 @@ def apply( ) ) - config = _validate(_load(path), backends) - explicit_tags = _explicit_cli_tags(cli_backend_specs) + data = _validate(_load(path), backends, repoagents, caches) + explicit_be = explicit_cli_tags(cli_backend_specs) + explicit_ra = explicit_cli_tags(cli_repoagent_specs) + explicit_ca = explicit_cli_tags(cli_cache_specs) messages = ["applying experimental --build-presets-file from '{}'".format(path)] - for be, props in config.items(): - # tag: an explicit CLI "--backend be:tag" wins over the file. - if "tag" in props: - if be in explicit_tags: - messages.append( - " backend '{}': tag '{}' from file ignored (CLI tag " - "wins)".format(be, props["tag"]) - ) - else: - backends[be] = props["tag"] - messages.append(" backend '{}': tag -> {}".format(be, props["tag"])) - - # org: drive BOTH the clone URL (backend_org_overrides) and the - # -DTRITON_REPO_ORGANIZATION arg (via the existing override map). If the - # CLI already pinned the org override, it wins for both. - if "org" in props: - cli_org = override_flags.get(be, {}).get(_REPO_ORG_ARG) - if cli_org is not None: - # CLI already pinned -DTRITON_REPO_ORGANIZATION for this backend. - # Honor "CLI wins" by treating the file "org" as absent: the -D - # keeps the CLI value and the clone org stays the global - # --github-organization, exactly as it would without the preset. + # core: cmake_args -> override channel, extra_cmake_args -> extra channel. + core = data.get("core", {}) + _pin_into( + override_core_flags, + set(override_core_flags), + core.get("cmake_args", {}), + "core", + messages, + ) + _pin_into( + extra_core_flags, + set(extra_core_flags), + core.get("extra_cmake_args", {}), + "core extra", + messages, + ) + + # backends: tag + per-flag override/extra (TRITON_REPO_ORGANIZATION also + # drives the clone org) + library_path. + for be, entry in data.get("backends", {}).items(): + _apply_tag(be, entry, backends, explicit_be, "backend", messages) + + cli_names = set(extra_backend_flags.get(be, {})) | set( + override_backend_flags.get(be, {}) + ) + override_bucket = override_backend_flags.setdefault(be, {}) + for flag, raw in entry.get("cmake_args", {}).items(): + value = _coerce_cmake_value( + _read_scalar(raw, "backend '{}'".format(be), allow_bool_num=True) + ) + if flag in cli_names: messages.append( - " backend '{}': org '{}' from file ignored (CLI set " - "-D{} explicitly)".format(be, props["org"], _REPO_ORG_ARG) + " backend '{}': cmake '{}' from file ignored (CLI " + "wins)".format(be, flag) ) - else: - org = props["org"] - backend_org_overrides[be] = org - override_flags.setdefault(be, {})[_REPO_ORG_ARG] = org - messages.append(" backend '{}': org -> {}".format(be, org)) + continue + override_bucket[flag] = value + if flag == _REPO_ORG_ARG and be not in _FIXED_ORG_BACKENDS: + backend_org_overrides[be] = value + messages.append(" backend '{}': cmake -D{}={}".format(be, flag, value)) + + _pin_into( + extra_backend_flags.setdefault(be, {}), + cli_names, + entry.get("extra_cmake_args", {}), + "backend '{}' extra".format(be), + messages, + ) - # library_path: a CLI-populated entry wins. - if "library_path" in props: + if "library_path" in entry: + lp = _read_scalar(entry["library_path"], "backend '{}'".format(be)) if be in library_paths: messages.append( " backend '{}': library_path from file ignored (CLI " "wins)".format(be) ) else: - library_paths[be] = props["library_path"] + library_paths[be] = lp + messages.append(" backend '{}': library_path -> {}".format(be, lp)) + + # repoagents / caches: pin the tag; cmake_args are visibility-only on load. + for section, tags, explicit, kind in ( + ("repoagents", repoagents, explicit_ra, "repoagent"), + ("caches", caches, explicit_ca, "cache"), + ): + for name, entry in data.get(section, {}).items(): + _apply_tag(name, entry, tags, explicit, kind, messages) + if entry.get("cmake_args"): messages.append( - " backend '{}': library_path -> {}".format( - be, props["library_path"] - ) + " {} '{}': cmake_args are informational and not " + "re-pinned".format(kind, name) ) - # extra / override cmake args: per-arg CLI-wins via setdefault semantics. - _apply_cmake_map( - be, props.get("extra_cmake_args"), extra_flags, "extra", messages - ) - _apply_cmake_map( - be, props.get("override_cmake_args"), override_flags, "override", messages - ) - return messages -def dump( +# --------------------------------------------------------------------------- # +# dump() -- write the annotated snapshot +# --------------------------------------------------------------------------- # +def _annotate(defs, provided, cli_keys, preset_keys): + return { + name: { + "value": value, + "source": flag_source( + name, + provided_dests=provided, + is_cli=name in cli_keys, + is_preset=name in preset_keys, + ), + } + for name, value in defs.items() + } + + +def _split_entry(arg_list, extra_names, provided, cli_keys, preset_keys): + # Split parsed -D flags into the override channel (built natively by build.py) + # and the extra/append channel (user-added flags), so a reload routes each + # back to the correct channel. + defs = parse_cmake_defs(arg_list) + cmake = {n: v for n, v in defs.items() if n not in extra_names} + extra = {n: v for n, v in defs.items() if n in extra_names} + entry = {"cmake_args": _annotate(cmake, provided, cli_keys, preset_keys)} + if extra: + entry["extra_cmake_args"] = _annotate(extra, provided, cli_keys, preset_keys) + return entry + + +def _tag_entry(name, tags, before, explicit): + if tags.get(name) != before.get(name): + source = "preset" + elif name in explicit: + source = "cli" + else: + source = "default" + return {"value": tags[name], "source": source} + + +def write_snapshot( path, *, + parser, + argv, + core_args, + backend_args, + repoagent_args, + cache_args, backends, + repoagents, + caches, + before_backends, + before_repoagents, + before_caches, + cli_backend_specs, + cli_repoagent_specs, + cli_cache_specs, + cli_extra_be, + cli_override_be, + cli_core, + extra_backend_flags, + override_backend_flags, + extra_core_flags, + override_core_flags, library_paths, - extra_flags, - override_flags, - backend_org_overrides, - default_org, ): - """Serialize the fully-resolved per-backend build configuration to ``path`` - as a build preset JSON. - - The output captures the effective values after defaults, command-line - options, and any applied --build-presets-file overrides have been merged, and is a - valid --build-presets-file input itself (round-trippable). Args mirror the build.py - structures passed to apply(); ``default_org`` is FLAGS.github_organization, - used for any backend without a per-backend org override. + """Build the provenance-annotated snapshot from the raw per-component cmake + argument lists (as returned by build.py's *_cmake_args helpers) and write it + to ``path``. Keeps all snapshot logic out of build.py.""" + provided = provided_options(parser, argv) + exp_be = explicit_cli_tags(cli_backend_specs) + exp_ra = explicit_cli_tags(cli_repoagent_specs) + exp_ca = explicit_cli_tags(cli_cache_specs) + + core_all = set(extra_core_flags) | set(override_core_flags) + snapshot = { + "_legend": { + "source": { + "cli": "explicit command-line value", + "preset": "value from --build-presets-file", + "default": "build.py default or derived value", + } + }, + "core": _split_entry( + core_args, set(extra_core_flags), provided, cli_core, core_all - cli_core + ), + "backends": {}, + "repoagents": {}, + "caches": {}, + } - Returns a list of human-readable log lines. Raises BuildPresetError if the - file cannot be written. - """ - out = {"backends": {}} - for be in sorted(backends): - entry = {"tag": backends[be]} + for be, args in backend_args.items(): + cli_keys = cli_extra_be.get(be, set()) | cli_override_be.get(be, set()) + all_keys = set(extra_backend_flags.get(be, {})) | set( + override_backend_flags.get(be, {}) + ) + entry = _split_entry( + args, + set(extra_backend_flags.get(be, {})), + provided, + cli_keys, + all_keys - cli_keys, + ) + entry["tag"] = _tag_entry(be, backends, before_backends, exp_be) if be in library_paths: - entry["library_path"] = library_paths[be] - extra = extra_flags.get(be) - if extra: - entry["extra_cmake_args"] = dict(extra) - - # Represent the clone organization faithfully and round-trippably. - # eff_org is the effective clone org; d is any -DTRITON_REPO_ORGANIZATION. - eff_org = backend_org_overrides.get(be, default_org) - override = dict(override_flags.get(be) or {}) - d = override.get(_REPO_ORG_ARG) - if d is not None and d == eff_org: - # Coupled (clone org == -D): express via "org" and drop the redundant - # -D so reload does not trip the org/override conflict check. - entry["org"] = eff_org - del override[_REPO_ORG_ARG] - elif d is not None: - # A -D override that differs from the (default) clone org: keep it as - # an override and omit "org" (clone org stays the global default). - pass - else: - entry["org"] = eff_org - - if override: - entry["override_cmake_args"] = override - out["backends"][be] = entry + entry["library_path"] = {"value": library_paths[be], "source": "cli"} + snapshot["backends"][be] = entry + + for ra, args in repoagent_args.items(): + snapshot["repoagents"][ra] = { + "tag": _tag_entry(ra, repoagents, before_repoagents, exp_ra), + "cmake_args": _annotate(parse_cmake_defs(args), provided, set(), set()), + } + for ca, args in cache_args.items(): + snapshot["caches"][ca] = { + "tag": _tag_entry(ca, caches, before_caches, exp_ca), + "cmake_args": _annotate(parse_cmake_defs(args), provided, set(), set()), + } + return dump(path, snapshot) + + +def dump(path, snapshot): + """Serialize a fully-built snapshot dict to ``path`` as JSON. Returns log + lines. Raises BuildPresetError if the file cannot be written.""" try: with open(path, "w") as f: - json.dump(out, f, indent=2, sort_keys=True) + json.dump(snapshot, f, indent=2, sort_keys=True) f.write("\n") except OSError as e: raise BuildPresetError( "--build-presets-file dump: cannot write '{}': {}".format(path, e) ) - - return ["wrote resolved build preset to '{}'".format(path)] + return ["wrote resolved build configuration snapshot to '{}'".format(path)] From 8830f1808d7d51a325d4670b8949172034a02996 Mon Sep 17 00:00:00 2001 From: "M. Chornyi" <99709299+mc-nv@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:10:03 -0700 Subject: [PATCH 3/4] test(build): add L2_build_presets pytest suite + SPDX header adoption Add qa/L2_build_presets validating the documented build-presets scenarios via build.py --dryrun (no GPU/container/build): snapshot sections + source annotation, CMAKE_INSTALL_PREFIX exclusion, provenance, extra-channel routing, env gate, example apply, round-trip reload, and validation errors. Runs from the server source tree, TRITON_BUILD_PY, or a shallow clone (bare container). Ships test.sh (deps + pytest --log-cli-level=INFO --junitxml, teed to a log), requirements.txt, and a README. SPDX-header adoption via a migrate-on-touch pre-commit hook (tools/add_spdx_header.py): scoped by file type so it brings each changed source file to the two-line SPDX header -- maintaining an existing one, replacing a legacy long-form NVIDIA BSD header in place (preserving comment style, shebang, and start year), or inserting one. Runs after add_copyright.py; the two coexist. Migrates the build_presets files and .pre-commit-config.yaml. Refs TRI-829. --- .pre-commit-config.yaml | 42 +-- qa/L2_build_presets/README.md | 46 +++ qa/L2_build_presets/build_presets_test.py | 377 ++++++++++++++++++++++ qa/L2_build_presets/requirements.txt | 3 + qa/L2_build_presets/test.sh | 25 ++ tools/add_spdx_header.py | 156 +++++++++ tools/build/build_presets.py | 27 +- 7 files changed, 626 insertions(+), 50 deletions(-) create mode 100644 qa/L2_build_presets/README.md create mode 100644 qa/L2_build_presets/build_presets_test.py create mode 100644 qa/L2_build_presets/requirements.txt create mode 100755 qa/L2_build_presets/test.sh create mode 100644 tools/add_spdx_header.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd6320fe0d..00e9c365de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,28 +1,5 @@ -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause repos: - repo: https://github.com/PyCQA/isort @@ -82,3 +59,18 @@ repos: stages: [pre-commit] verbose: true require_serial: true + # Incremental SPDX-header adoption: migrate each source file to the two-line + # SPDX header the first time it is touched. pre-commit runs hooks only on the + # files staged in a commit, so scoping by file type (below) -- not by directory + # -- rolls SPDX out gradually as files change. The hook maintains an existing + # SPDX header, replaces a legacy long-form NVIDIA BSD header in place, or + # inserts one. It runs after add-license above, which only maintains the + # copyright year on the string both headers share. + - id: add-spdx-license + name: Add SPDX License Header + entry: python tools/add_spdx_header.py + language: python + files: \.(py|pyi|sh|bash|yaml|yml|cc|cpp|cxx|h|hpp|cu|cuh)$ + stages: [pre-commit] + verbose: true + require_serial: true diff --git a/qa/L2_build_presets/README.md b/qa/L2_build_presets/README.md new file mode 100644 index 0000000000..870e1864d5 --- /dev/null +++ b/qa/L2_build_presets/README.md @@ -0,0 +1,46 @@ + + +# L2_build_presets + +Validates the experimental **build presets** feature +(`docs/customization_guide/build.md`) by running `build.py --dryrun` and checking +the generated `build_presets.json`. No GPU, container, or real build needed. + +## Run + +```bash +cd qa/L2_build_presets +bash test.sh # installs deps, runs pytest, writes logs +``` + +Or directly: `python3 -m pytest build_presets_test.py`. + +## Finding build.py + +`build.py` lives in the server repo. It is located in-tree (a checkout), or +cloned when only this directory is present. Override via: + +| Env var | Meaning | Default | +|---|---|---| +| `TRITON_BUILD_PY` | explicit path to `build.py` | — | +| `TRITON_SERVER_REPO` | repo to clone when not found | `.../triton-inference-server/server.git` | +| `TRITON_SERVER_BRANCH_NAME` | branch/tag to clone — **must exist on the remote** | `main` | + +Bare container, mounting only this directory: + +```bash +cd server/ +docker run --rm -v "$PWD":/workspace -w /workspace \ + -e TRITON_SERVER_BRANCH_NAME= \ + -w /workspace/qa/L2_build_presets \ + nvcr.io/nvidia/tritonserver:26.06-py3 python3 build_presets_test.py +``` + +## Output + +`test.sh` writes `build_presets_test.log` (full console) and +`build_presets_test.report.xml` (JUnit) — both git-ignored — and prints the +`*** Test Passed ***` / `*** Test FAILED ***` markers. diff --git a/qa/L2_build_presets/build_presets_test.py b/qa/L2_build_presets/build_presets_test.py new file mode 100644 index 0000000000..6729bb3ff5 --- /dev/null +++ b/qa/L2_build_presets/build_presets_test.py @@ -0,0 +1,377 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause + +"""Validate the documented experimental build-presets scenarios. + +Reference: ``server/docs/customization_guide/build.md`` -> "Experimental: Build +Presets". Every test drives ``build.py`` in ``--dryrun`` mode only -- no GPU, no +container, and no real build are required; build.py just generates the +``cmake_build`` script and the ``build_presets.json`` snapshot, which the tests +inspect. + +Self-sufficient: it finds ``build.py`` in-tree (source checkout), via +``TRITON_BUILD_PY``, or by cloning the server repo when only this directory is +present (bare container). See the README to run it. +""" + +import json +import logging +import os +import shlex +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +# build.py command diagnostics; shown when pytest logs at INFO level +# (test.sh runs with --log-cli-level=INFO). +LOGGER = logging.getLogger("l2_build_presets") + +# build.py lives in the server repo. When only this test directory is available +# (e.g. a bare container), it is cloned; override the source with these env vars. +# The ref must contain the experimental build-presets feature. +SERVER_REPO = os.environ.get( + "TRITON_SERVER_REPO", "https://github.com/triton-inference-server/server.git" +) +SERVER_BRANCH_NAME = os.environ.get("TRITON_SERVER_BRANCH_NAME", "main") + + +def _resolve_server_dir(): + override = os.environ.get("TRITON_BUILD_PY") + if override: + return Path(override).resolve().parent + for parent in Path(__file__).resolve().parents: + if (parent / "build.py").is_file(): + LOGGER.info("build.py found in-tree: %s", parent) + return parent + dest = Path(tempfile.gettempdir()) / "triton-server-under-test" + if not (dest / "build.py").is_file(): + LOGGER.info( + "build.py not in-tree; cloning %s@%s", SERVER_REPO, SERVER_BRANCH_NAME + ) + shutil.rmtree(dest, ignore_errors=True) + try: + subprocess.run( + [ + "git", + "clone", + "--depth", + "1", + "--branch", + SERVER_BRANCH_NAME, + SERVER_REPO, + str(dest), + ], + check=True, + capture_output=True, + text=True, + ) + except (OSError, subprocess.CalledProcessError) as exc: + detail = getattr(exc, "stderr", "") or str(exc) + raise RuntimeError( + "build.py not found in-tree and clone of {}@{} failed: {}\nSet " + "TRITON_BUILD_PY, or TRITON_SERVER_REPO / TRITON_SERVER_BRANCH_NAME.".format( + SERVER_REPO, SERVER_BRANCH_NAME, detail + ) + ) + return dest + + +_SERVER_DIR_CACHE = None + + +def server_dir(): + """Locate the server repo root holding build.py: ``TRITON_BUILD_PY`` override + -> in-tree (upward search) -> a shallow clone of ``SERVER_REPO`` at + ``SERVER_BRANCH_NAME``. The resolution (success OR failure) is cached, so a + failed clone is attempted once and then re-raised fast for the remaining + tests instead of re-cloning per test.""" + global _SERVER_DIR_CACHE + if isinstance(_SERVER_DIR_CACHE, BaseException): + raise _SERVER_DIR_CACHE + if _SERVER_DIR_CACHE is None: + try: + _SERVER_DIR_CACHE = _resolve_server_dir() + except Exception as exc: + _SERVER_DIR_CACHE = exc + raise + return _SERVER_DIR_CACHE + + +def build_py(): + return server_dir() / "build.py" + + +def example_preset(): + return server_dir() / "tools" / "build" / "build_presets.example.json" + + +# A representative invocation that exercises every snapshot section. +BASE_ARGS = [ + "--backend", "ensemble", + "--backend", "python", + "--backend", "onnxruntime", + "--repoagent", "checksum", + "--cache", "local", + "--enable-logging", + "--enable-stats", + "--enable-gpu", + "--endpoint", "http", + "--endpoint", "grpc", +] # fmt: skip + + +def run_build(build_dir, extra_args=(), *, experimental=True, verbose=False): + """Run ``build.py`` in --dryrun mode into ``build_dir``. Returns the + completed process (returncode / stdout / stderr).""" + Path(build_dir).mkdir(parents=True, exist_ok=True) + cmd = [ + sys.executable, + str(build_py()), + "--no-container-build", + "--build-dir", + str(build_dir), + ] + cmd += list(extra_args) + cmd += ["--dryrun", "-v" if verbose else "-q"] + env = dict(os.environ) + if experimental: + env["TRITON_BUILD_EXPERIMENTAL"] = "1" + else: + env.pop("TRITON_BUILD_EXPERIMENTAL", None) + # Log a copy-paste-runnable command (env prefix + full argv, shell-quoted). + prefix = "TRITON_BUILD_EXPERIMENTAL=1 " if experimental else "" + LOGGER.info("run (cwd=%s):\n %s%s", server_dir(), prefix, shlex.join(cmd)) + proc = subprocess.run( + cmd, cwd=str(server_dir()), env=env, capture_output=True, text=True + ) + LOGGER.info(" -> returncode %d", proc.returncode) + return proc + + +def load_snapshot(build_dir): + """Parse the build_presets.json snapshot from a --dryrun run.""" + return json.loads((Path(build_dir) / "build_presets.json").read_text()) + + +def read_cmake_build(build_dir): + """Read the generated cmake_build script from a --dryrun run.""" + return (Path(build_dir) / "cmake_build").read_text() + + +def dump_snapshot(build_dir, extra_args=()): + """Run a dump with BASE_ARGS + extra_args and return the parsed snapshot.""" + proc = run_build(build_dir, list(extra_args) + BASE_ARGS) + assert proc.returncode == 0, proc.stderr + return load_snapshot(build_dir) + + +# --------------------------------------------------------------------------- # +# Documented-scenario tests +# --------------------------------------------------------------------------- # +def test_dryrun_generates_snapshot_with_all_sections(tmp_path): + """--dryrun writes build_presets.json covering all four sections: core, + backends, repoagents, caches.""" + snap = dump_snapshot(tmp_path) + for section in ("core", "backends", "repoagents", "caches"): + assert section in snap, section + assert "onnxruntime" in snap["backends"] + assert "checksum" in snap["repoagents"] + assert "local" in snap["caches"] + + +def test_every_flag_is_source_annotated(tmp_path): + """Every cmake flag in the snapshot carries a {value, source} pair, and each + source is one of cli/preset/default.""" + snap = dump_snapshot(tmp_path) + sources = set() + for flag, entry in snap["core"]["cmake_args"].items(): + assert set(entry) == {"value", "source"}, flag + sources.add(entry["source"]) + assert sources <= {"cli", "preset", "default"} + + +def test_provenance_cli_vs_default(tmp_path): + """Provenance is correct: a flag whose CLI option was passed (--enable-gpu) + is labeled 'cli'; an unset flag (--build-type) is labeled 'default'.""" + core = dump_snapshot(tmp_path)["core"]["cmake_args"] + assert core["TRITON_ENABLE_GPU"]["source"] == "cli" + assert core["TRITON_ENABLE_GPU"]["value"] == "ON" + assert core["CMAKE_BUILD_TYPE"]["source"] == "default" + + +def test_install_prefix_excluded_from_snapshot(tmp_path): + """CMAKE_INSTALL_PREFIX (an absolute build-dir path) is excluded from the + snapshot so it is not pinned across build directories.""" + snap = dump_snapshot(tmp_path) + assert "CMAKE_INSTALL_PREFIX" not in snap["core"]["cmake_args"] + + +def test_user_added_flag_lands_in_extra_channel(tmp_path): + """A user-added flag (--extra-backend-cmake-arg) that build.py does not emit + natively is recorded in the backend's extra_cmake_args channel, not + cmake_args.""" + snap = dump_snapshot( + tmp_path, + extra_args=[ + "--extra-backend-cmake-arg", + "python:TRITON_BOOST_URL=https://example.com/boost.tar.gz", + ], + ) + py = snap["backends"]["python"] + assert "TRITON_BOOST_URL" in py.get("extra_cmake_args", {}) + assert py["extra_cmake_args"]["TRITON_BOOST_URL"]["source"] == "cli" + + +def test_env_gate_rejects_without_experimental(tmp_path): + """The experimental gate holds: without TRITON_BUILD_EXPERIMENTAL=1, + --build-presets-file errors out and no snapshot is written.""" + proc = run_build( + tmp_path, + BASE_ARGS + ["--build-presets-file", str(example_preset())], + experimental=False, + ) + assert proc.returncode != 0 + assert "TRITON_BUILD_EXPERIMENTAL" in proc.stderr + assert not (tmp_path / "build_presets.json").exists() + + +def test_example_preset_applies(tmp_path): + """The shipped example preset applies: onnxruntime is cloned at the file's + tag from the file's TRITON_REPO_ORGANIZATION.""" + proc = run_build( + tmp_path, + [ + "--backend", + "ensemble", + "--backend", + "python", + "--backend", + "pytorch", + "--backend", + "onnxruntime", + "--enable-gpu", + "--build-presets-file", + str(example_preset()), + ], + ) + assert proc.returncode == 0, proc.stderr + cmake = read_cmake_build(tmp_path) + assert "triton-inference-server/onnxruntime_backend.git" in cmake + assert "-b r25.08_fix" in cmake # onnxruntime tag from the example + + +def test_roundtrip_reload_pins_flags(tmp_path): + """Round-trip: a dumped snapshot reloads and pins its flags -- the extra arg + and core GPU=ON reproduce even without the original CLI flags.""" + dump_dir = tmp_path / "dump" + proc = run_build( + dump_dir, + BASE_ARGS + + [ + "--extra-backend-cmake-arg", + "python:TRITON_BOOST_URL=https://example.com/boost.tar.gz", + ], + ) + assert proc.returncode == 0, proc.stderr + preset = dump_dir / "build_presets.json" + + reload_dir = tmp_path / "reload" + proc = run_build( + reload_dir, + [ + "--backend", + "ensemble", + "--backend", + "python", + "--backend", + "onnxruntime", + "--repoagent", + "checksum", + "--cache", + "local", + "--build-presets-file", + str(preset), + ], + ) + assert proc.returncode == 0, proc.stderr + cmake = read_cmake_build(reload_dir) + assert "TRITON_BOOST_URL=https://example.com/boost.tar.gz" in cmake + assert "-DTRITON_ENABLE_GPU:BOOL=ON" in cmake # pinned core flag + + +def test_cli_tag_wins_over_preset(tmp_path): + """Precedence: an explicit CLI '--backend onnxruntime:from_cli' tag wins over + the preset file's tag.""" + preset = tmp_path / "preset.json" + preset.write_text(json.dumps({"backends": {"onnxruntime": {"tag": "from_preset"}}})) + proc = run_build( + tmp_path, + [ + "--backend", + "ensemble", + "--backend", + "onnxruntime:from_cli", + "--build-presets-file", + str(preset), + ], + ) + assert proc.returncode == 0, proc.stderr + cmake = read_cmake_build(tmp_path) + assert "-b from_cli " in cmake + assert "from_preset" not in cmake + + +def test_backend_not_in_build_is_rejected(tmp_path): + """Validation: a preset naming a backend not included in the build fails with + a clear 'not included in the build' error.""" + preset = tmp_path / "preset.json" + preset.write_text(json.dumps({"backends": {"nope": {"tag": "x"}}})) + proc = run_build( + tmp_path, ["--backend", "ensemble", "--build-presets-file", str(preset)] + ) + assert proc.returncode != 0 + assert "not included in the build" in proc.stderr + + +def test_unknown_component_key_is_rejected(tmp_path): + """Validation: a preset with an unknown per-component key (typo) fails with a + clear 'unknown key' error.""" + preset = tmp_path / "preset.json" + preset.write_text(json.dumps({"backends": {"onnxruntime": {"taag": "x"}}})) + proc = run_build( + tmp_path, + [ + "--backend", + "ensemble", + "--backend", + "onnxruntime", + "--build-presets-file", + str(preset), + ], + ) + assert proc.returncode != 0 + assert "unknown key" in proc.stderr + + +if __name__ == "__main__": + # Self-sufficient standalone entry (`python3 build_presets_test.py`): install + # this suite's deps (pytest + build.py's distro/requests), then run the tests. + # build.py itself is located/cloned by server_dir(). Set L2_NO_BOOTSTRAP=1 to + # skip the install (e.g. deps already present). + if not os.environ.get("L2_NO_BOOTSTRAP"): + subprocess.run( + [ + sys.executable, + "-m", + "pip", + "install", + "-q", + "-r", + str(Path(__file__).with_name("requirements.txt")), + ] + ) + import pytest + + raise SystemExit(pytest.main([__file__])) diff --git a/qa/L2_build_presets/requirements.txt b/qa/L2_build_presets/requirements.txt new file mode 100644 index 0000000000..564d93af84 --- /dev/null +++ b/qa/L2_build_presets/requirements.txt @@ -0,0 +1,3 @@ +distro +pytest +requests diff --git a/qa/L2_build_presets/test.sh b/qa/L2_build_presets/test.sh new file mode 100755 index 0000000000..329c100aea --- /dev/null +++ b/qa/L2_build_presets/test.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause + +# Validate the documented build-presets scenarios via build.py --dryrun (no GPU, +# container, or real build). Runs from the server source tree; see README.md. + +TEST_LOG="build_presets_test.log" +TEST_REPORT="build_presets_test.report.xml" +RET=0 +python3 -m pip install --quiet -r requirements.txt >/dev/null 2>&1 || true + +# tee: stream to the console AND persist a full log for CI archiving. +# --junitxml= writes machine-readable results (NOTE: the '=' is required; +# 'pytest --junitxml FILE.py' would treat FILE.py as the output path). +# PIPESTATUS[0] captures pytest's exit code, not tee's. +python3 -m pytest -v --log-cli-level=INFO --junitxml="$TEST_REPORT" build_presets_test.py 2>&1 | tee "$TEST_LOG" +RET=${PIPESTATUS[0]} + +if [ $RET -eq 0 ]; then + echo -e "\n***\n*** Test Passed\n***" +else + echo -e "\n***\n*** Test FAILED\n***" +fi +exit $RET diff --git a/tools/add_spdx_header.py b/tools/add_spdx_header.py new file mode 100644 index 0000000000..5ce5badd90 --- /dev/null +++ b/tools/add_spdx_header.py @@ -0,0 +1,156 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause + +"""Insert / maintain / migrate to an SPDX license header on source files. + +Pre-commit hook that adopts the two-line SPDX header on every file it is run +against. Because pre-commit runs hooks on the files staged in a commit, scoping +this hook by file type (rather than by directory) migrates each source file to +SPDX *the first time it is touched* -- a low-risk, incremental rollout. + +The header uses a copyright *year range*, mirroring the repo convention (the +``LICENSE`` file and ``add_copyright.py`` use ``-``):: + + # SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: BSD-3-Clause + +Per file, the hook does exactly one of: + +* **Maintain** -- already SPDX: bump the copyright end year to the current year. +* **Migrate** -- carries the legacy long-form NVIDIA BSD header: replace that + whole block in place with the two SPDX lines, preserving the comment style + (``#`` or ``//``), the shebang, and the block's start year. +* **Insert** -- no NVIDIA header: add the SPDX header (after a shebang if any). + +Idempotent, and coexists with ``add_copyright.py`` (which runs first and only +maintains the copyright year on the legacy/SPDX string it recognizes). +""" + +import os +import re +import sys +from datetime import datetime + +ROOT_DIR = os.path.join(os.path.dirname(__file__), os.pardir) +LICENSE_PATH = os.path.join(ROOT_DIR, "LICENSE") +CURRENT_YEAR = str(datetime.now().year) + +_SPDX_MARKER = "SPDX-License-Identifier" +_LICENSE_ID = "BSD-3-Clause" + +# Start year of the project-wide LICENSE (used only when inserting into a file +# that has no NVIDIA copyright at all). +_LICENSE_YEAR_PAT = re.compile(r"Copyright \(c\) (\d{4})(?:-\d{4})?, NVIDIA") +# Existing SPDX copyright line (for year maintenance). +_SPDX_YEAR_PAT = re.compile( + r"(SPDX-FileCopyrightText: Copyright \(c\) )(\d{4})(?:-\d{4})?(, NVIDIA)" +) +# First line of a legacy long-form NVIDIA BSD header (captures comment prefix + +# start year). The block ends at the standard BSD "SUCH DAMAGE" line. +_LEGACY_COPY_PAT = re.compile( + r"^(#|//) ?Copyright(?: \(c\))? (\d{4})(?:-\d{4})?,? NVIDIA CORPORATION" +) +_LEGACY_END_MARK = "POSSIBILITY OF SUCH DAMAGE" + +_CPP_EXTS = (".cc", ".cpp", ".cxx", ".h", ".hpp", ".cu", ".cuh") + + +def _year_range(start): + return start if start == CURRENT_YEAR else "{}-{}".format(start, CURRENT_YEAR) + + +def _license_start_year(): + try: + with open(LICENSE_PATH, "r", encoding="utf-8") as f: + match = _LICENSE_YEAR_PAT.search(f.read()) + if match: + return match.group(1) + except OSError: + pass + return CURRENT_YEAR + + +def _spdx_lines(prefix, years): + return ( + "{p} SPDX-FileCopyrightText: Copyright (c) {y}, NVIDIA CORPORATION " + "& AFFILIATES. All rights reserved.\n" + "{p} SPDX-License-Identifier: {lic}\n".format( + p=prefix, y=years, lic=_LICENSE_ID + ) + ) + + +def _maintain(content): + def _bump(match): + return match.group(1) + _year_range(match.group(2)) + match.group(3) + + return _SPDX_YEAR_PAT.sub(_bump, content) + + +def _migrate_legacy(content): + """Replace a legacy long-form NVIDIA BSD header with the SPDX lines. Returns + the new content, or None if no legacy header is found.""" + lines = content.splitlines(keepends=True) + start = prefix = start_year = None + for i, line in enumerate(lines): + match = _LEGACY_COPY_PAT.match(line) + if match: + start, prefix, start_year = i, match.group(1), match.group(2) + break + if start is None: + return None + end = None + for j in range(start, len(lines)): + if _LEGACY_END_MARK in lines[j]: + end = j + break + if end is None: + return None + header = _spdx_lines(prefix, _year_range(start_year)) + return "".join(lines[:start]) + header + "".join(lines[end + 1 :]) + + +def _insert(path, content): + prefix = "//" if path.endswith(_CPP_EXTS) else "#" + header = _spdx_lines(prefix, _year_range(_license_start_year())) + "\n" + lines = content.splitlines(keepends=True) + if lines and lines[0].startswith("#!"): + return lines[0] + header + "".join(lines[1:]) + return header + content + + +def process(path): + """Bring ``path`` to an SPDX header (maintain/migrate/insert). Returns True if + the file was modified.""" + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + except (OSError, UnicodeDecodeError): + return False + + if _SPDX_MARKER in content: + updated = _maintain(content) + else: + updated = _migrate_legacy(content) + if updated is None: + updated = _insert(path, content) + + if updated == content: + return False + with open(path, "w", encoding="utf-8") as f: + f.write(updated) + return True + + +def main(argv): + changed = False + for path in argv[1:]: + if process(path): + print("Updated SPDX header: {}".format(path)) + changed = True + # Non-zero exit tells pre-commit the file was modified so it re-stages. + return 1 if changed else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tools/build/build_presets.py b/tools/build/build_presets.py index fe7c87a87d..1e7788feee 100644 --- a/tools/build/build_presets.py +++ b/tools/build/build_presets.py @@ -1,28 +1,5 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause """Experimental build presets for build.py. From 6f8c3922a3a8a9c4ef8cb7b42977fc112698acf4 Mon Sep 17 00:00:00 2001 From: "M. Chornyi" <99709299+mc-nv@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:10:03 -0700 Subject: [PATCH 4/4] test(build): add L2_build_presets pytest suite + SPDX header adoption Add qa/L2_build_presets validating the documented build-presets scenarios via build.py --dryrun (no GPU/container/build): snapshot sections + source annotation, CMAKE_INSTALL_PREFIX exclusion, provenance, extra-channel routing, env gate, example apply, round-trip reload, and validation errors. Runs from the server source tree, TRITON_BUILD_PY, or a shallow clone (bare container). Ships test.sh (deps + pytest --log-cli-level=INFO --junitxml, teed to a log), requirements.txt, and a README. SPDX-header adoption via a migrate-on-touch pre-commit hook (tools/add_spdx_header.py): scoped by file type so it brings each changed source file to the two-line SPDX header -- maintaining an existing one, replacing a legacy long-form NVIDIA BSD header in place (preserving comment style, shebang, and start year), or inserting one. Runs after add_copyright.py; the two coexist. Migrates the build_presets files and .pre-commit-config.yaml. Refs TRI-829. --- .pre-commit-config.yaml | 42 +-- build.py | 27 +- qa/L2_build_presets/README.md | 46 +++ qa/L2_build_presets/build_presets_test.py | 377 ++++++++++++++++++++++ qa/L2_build_presets/requirements.txt | 3 + qa/L2_build_presets/test.sh | 25 ++ tools/add_spdx_header.py | 156 +++++++++ tools/build/build_presets.py | 27 +- 8 files changed, 628 insertions(+), 75 deletions(-) create mode 100644 qa/L2_build_presets/README.md create mode 100644 qa/L2_build_presets/build_presets_test.py create mode 100644 qa/L2_build_presets/requirements.txt create mode 100755 qa/L2_build_presets/test.sh create mode 100644 tools/add_spdx_header.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd6320fe0d..00e9c365de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,28 +1,5 @@ -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause repos: - repo: https://github.com/PyCQA/isort @@ -82,3 +59,18 @@ repos: stages: [pre-commit] verbose: true require_serial: true + # Incremental SPDX-header adoption: migrate each source file to the two-line + # SPDX header the first time it is touched. pre-commit runs hooks only on the + # files staged in a commit, so scoping by file type (below) -- not by directory + # -- rolls SPDX out gradually as files change. The hook maintains an existing + # SPDX header, replaces a legacy long-form NVIDIA BSD header in place, or + # inserts one. It runs after add-license above, which only maintains the + # copyright year on the string both headers share. + - id: add-spdx-license + name: Add SPDX License Header + entry: python tools/add_spdx_header.py + language: python + files: \.(py|pyi|sh|bash|yaml|yml|cc|cpp|cxx|h|hpp|cu|cuh)$ + stages: [pre-commit] + verbose: true + require_serial: true diff --git a/build.py b/build.py index a497a527cf..b473b76739 100755 --- a/build.py +++ b/build.py @@ -1,29 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause import argparse import importlib.util diff --git a/qa/L2_build_presets/README.md b/qa/L2_build_presets/README.md new file mode 100644 index 0000000000..870e1864d5 --- /dev/null +++ b/qa/L2_build_presets/README.md @@ -0,0 +1,46 @@ + + +# L2_build_presets + +Validates the experimental **build presets** feature +(`docs/customization_guide/build.md`) by running `build.py --dryrun` and checking +the generated `build_presets.json`. No GPU, container, or real build needed. + +## Run + +```bash +cd qa/L2_build_presets +bash test.sh # installs deps, runs pytest, writes logs +``` + +Or directly: `python3 -m pytest build_presets_test.py`. + +## Finding build.py + +`build.py` lives in the server repo. It is located in-tree (a checkout), or +cloned when only this directory is present. Override via: + +| Env var | Meaning | Default | +|---|---|---| +| `TRITON_BUILD_PY` | explicit path to `build.py` | — | +| `TRITON_SERVER_REPO` | repo to clone when not found | `.../triton-inference-server/server.git` | +| `TRITON_SERVER_BRANCH_NAME` | branch/tag to clone — **must exist on the remote** | `main` | + +Bare container, mounting only this directory: + +```bash +cd server/ +docker run --rm -v "$PWD":/workspace -w /workspace \ + -e TRITON_SERVER_BRANCH_NAME= \ + -w /workspace/qa/L2_build_presets \ + nvcr.io/nvidia/tritonserver:26.06-py3 python3 build_presets_test.py +``` + +## Output + +`test.sh` writes `build_presets_test.log` (full console) and +`build_presets_test.report.xml` (JUnit) — both git-ignored — and prints the +`*** Test Passed ***` / `*** Test FAILED ***` markers. diff --git a/qa/L2_build_presets/build_presets_test.py b/qa/L2_build_presets/build_presets_test.py new file mode 100644 index 0000000000..6729bb3ff5 --- /dev/null +++ b/qa/L2_build_presets/build_presets_test.py @@ -0,0 +1,377 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause + +"""Validate the documented experimental build-presets scenarios. + +Reference: ``server/docs/customization_guide/build.md`` -> "Experimental: Build +Presets". Every test drives ``build.py`` in ``--dryrun`` mode only -- no GPU, no +container, and no real build are required; build.py just generates the +``cmake_build`` script and the ``build_presets.json`` snapshot, which the tests +inspect. + +Self-sufficient: it finds ``build.py`` in-tree (source checkout), via +``TRITON_BUILD_PY``, or by cloning the server repo when only this directory is +present (bare container). See the README to run it. +""" + +import json +import logging +import os +import shlex +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +# build.py command diagnostics; shown when pytest logs at INFO level +# (test.sh runs with --log-cli-level=INFO). +LOGGER = logging.getLogger("l2_build_presets") + +# build.py lives in the server repo. When only this test directory is available +# (e.g. a bare container), it is cloned; override the source with these env vars. +# The ref must contain the experimental build-presets feature. +SERVER_REPO = os.environ.get( + "TRITON_SERVER_REPO", "https://github.com/triton-inference-server/server.git" +) +SERVER_BRANCH_NAME = os.environ.get("TRITON_SERVER_BRANCH_NAME", "main") + + +def _resolve_server_dir(): + override = os.environ.get("TRITON_BUILD_PY") + if override: + return Path(override).resolve().parent + for parent in Path(__file__).resolve().parents: + if (parent / "build.py").is_file(): + LOGGER.info("build.py found in-tree: %s", parent) + return parent + dest = Path(tempfile.gettempdir()) / "triton-server-under-test" + if not (dest / "build.py").is_file(): + LOGGER.info( + "build.py not in-tree; cloning %s@%s", SERVER_REPO, SERVER_BRANCH_NAME + ) + shutil.rmtree(dest, ignore_errors=True) + try: + subprocess.run( + [ + "git", + "clone", + "--depth", + "1", + "--branch", + SERVER_BRANCH_NAME, + SERVER_REPO, + str(dest), + ], + check=True, + capture_output=True, + text=True, + ) + except (OSError, subprocess.CalledProcessError) as exc: + detail = getattr(exc, "stderr", "") or str(exc) + raise RuntimeError( + "build.py not found in-tree and clone of {}@{} failed: {}\nSet " + "TRITON_BUILD_PY, or TRITON_SERVER_REPO / TRITON_SERVER_BRANCH_NAME.".format( + SERVER_REPO, SERVER_BRANCH_NAME, detail + ) + ) + return dest + + +_SERVER_DIR_CACHE = None + + +def server_dir(): + """Locate the server repo root holding build.py: ``TRITON_BUILD_PY`` override + -> in-tree (upward search) -> a shallow clone of ``SERVER_REPO`` at + ``SERVER_BRANCH_NAME``. The resolution (success OR failure) is cached, so a + failed clone is attempted once and then re-raised fast for the remaining + tests instead of re-cloning per test.""" + global _SERVER_DIR_CACHE + if isinstance(_SERVER_DIR_CACHE, BaseException): + raise _SERVER_DIR_CACHE + if _SERVER_DIR_CACHE is None: + try: + _SERVER_DIR_CACHE = _resolve_server_dir() + except Exception as exc: + _SERVER_DIR_CACHE = exc + raise + return _SERVER_DIR_CACHE + + +def build_py(): + return server_dir() / "build.py" + + +def example_preset(): + return server_dir() / "tools" / "build" / "build_presets.example.json" + + +# A representative invocation that exercises every snapshot section. +BASE_ARGS = [ + "--backend", "ensemble", + "--backend", "python", + "--backend", "onnxruntime", + "--repoagent", "checksum", + "--cache", "local", + "--enable-logging", + "--enable-stats", + "--enable-gpu", + "--endpoint", "http", + "--endpoint", "grpc", +] # fmt: skip + + +def run_build(build_dir, extra_args=(), *, experimental=True, verbose=False): + """Run ``build.py`` in --dryrun mode into ``build_dir``. Returns the + completed process (returncode / stdout / stderr).""" + Path(build_dir).mkdir(parents=True, exist_ok=True) + cmd = [ + sys.executable, + str(build_py()), + "--no-container-build", + "--build-dir", + str(build_dir), + ] + cmd += list(extra_args) + cmd += ["--dryrun", "-v" if verbose else "-q"] + env = dict(os.environ) + if experimental: + env["TRITON_BUILD_EXPERIMENTAL"] = "1" + else: + env.pop("TRITON_BUILD_EXPERIMENTAL", None) + # Log a copy-paste-runnable command (env prefix + full argv, shell-quoted). + prefix = "TRITON_BUILD_EXPERIMENTAL=1 " if experimental else "" + LOGGER.info("run (cwd=%s):\n %s%s", server_dir(), prefix, shlex.join(cmd)) + proc = subprocess.run( + cmd, cwd=str(server_dir()), env=env, capture_output=True, text=True + ) + LOGGER.info(" -> returncode %d", proc.returncode) + return proc + + +def load_snapshot(build_dir): + """Parse the build_presets.json snapshot from a --dryrun run.""" + return json.loads((Path(build_dir) / "build_presets.json").read_text()) + + +def read_cmake_build(build_dir): + """Read the generated cmake_build script from a --dryrun run.""" + return (Path(build_dir) / "cmake_build").read_text() + + +def dump_snapshot(build_dir, extra_args=()): + """Run a dump with BASE_ARGS + extra_args and return the parsed snapshot.""" + proc = run_build(build_dir, list(extra_args) + BASE_ARGS) + assert proc.returncode == 0, proc.stderr + return load_snapshot(build_dir) + + +# --------------------------------------------------------------------------- # +# Documented-scenario tests +# --------------------------------------------------------------------------- # +def test_dryrun_generates_snapshot_with_all_sections(tmp_path): + """--dryrun writes build_presets.json covering all four sections: core, + backends, repoagents, caches.""" + snap = dump_snapshot(tmp_path) + for section in ("core", "backends", "repoagents", "caches"): + assert section in snap, section + assert "onnxruntime" in snap["backends"] + assert "checksum" in snap["repoagents"] + assert "local" in snap["caches"] + + +def test_every_flag_is_source_annotated(tmp_path): + """Every cmake flag in the snapshot carries a {value, source} pair, and each + source is one of cli/preset/default.""" + snap = dump_snapshot(tmp_path) + sources = set() + for flag, entry in snap["core"]["cmake_args"].items(): + assert set(entry) == {"value", "source"}, flag + sources.add(entry["source"]) + assert sources <= {"cli", "preset", "default"} + + +def test_provenance_cli_vs_default(tmp_path): + """Provenance is correct: a flag whose CLI option was passed (--enable-gpu) + is labeled 'cli'; an unset flag (--build-type) is labeled 'default'.""" + core = dump_snapshot(tmp_path)["core"]["cmake_args"] + assert core["TRITON_ENABLE_GPU"]["source"] == "cli" + assert core["TRITON_ENABLE_GPU"]["value"] == "ON" + assert core["CMAKE_BUILD_TYPE"]["source"] == "default" + + +def test_install_prefix_excluded_from_snapshot(tmp_path): + """CMAKE_INSTALL_PREFIX (an absolute build-dir path) is excluded from the + snapshot so it is not pinned across build directories.""" + snap = dump_snapshot(tmp_path) + assert "CMAKE_INSTALL_PREFIX" not in snap["core"]["cmake_args"] + + +def test_user_added_flag_lands_in_extra_channel(tmp_path): + """A user-added flag (--extra-backend-cmake-arg) that build.py does not emit + natively is recorded in the backend's extra_cmake_args channel, not + cmake_args.""" + snap = dump_snapshot( + tmp_path, + extra_args=[ + "--extra-backend-cmake-arg", + "python:TRITON_BOOST_URL=https://example.com/boost.tar.gz", + ], + ) + py = snap["backends"]["python"] + assert "TRITON_BOOST_URL" in py.get("extra_cmake_args", {}) + assert py["extra_cmake_args"]["TRITON_BOOST_URL"]["source"] == "cli" + + +def test_env_gate_rejects_without_experimental(tmp_path): + """The experimental gate holds: without TRITON_BUILD_EXPERIMENTAL=1, + --build-presets-file errors out and no snapshot is written.""" + proc = run_build( + tmp_path, + BASE_ARGS + ["--build-presets-file", str(example_preset())], + experimental=False, + ) + assert proc.returncode != 0 + assert "TRITON_BUILD_EXPERIMENTAL" in proc.stderr + assert not (tmp_path / "build_presets.json").exists() + + +def test_example_preset_applies(tmp_path): + """The shipped example preset applies: onnxruntime is cloned at the file's + tag from the file's TRITON_REPO_ORGANIZATION.""" + proc = run_build( + tmp_path, + [ + "--backend", + "ensemble", + "--backend", + "python", + "--backend", + "pytorch", + "--backend", + "onnxruntime", + "--enable-gpu", + "--build-presets-file", + str(example_preset()), + ], + ) + assert proc.returncode == 0, proc.stderr + cmake = read_cmake_build(tmp_path) + assert "triton-inference-server/onnxruntime_backend.git" in cmake + assert "-b r25.08_fix" in cmake # onnxruntime tag from the example + + +def test_roundtrip_reload_pins_flags(tmp_path): + """Round-trip: a dumped snapshot reloads and pins its flags -- the extra arg + and core GPU=ON reproduce even without the original CLI flags.""" + dump_dir = tmp_path / "dump" + proc = run_build( + dump_dir, + BASE_ARGS + + [ + "--extra-backend-cmake-arg", + "python:TRITON_BOOST_URL=https://example.com/boost.tar.gz", + ], + ) + assert proc.returncode == 0, proc.stderr + preset = dump_dir / "build_presets.json" + + reload_dir = tmp_path / "reload" + proc = run_build( + reload_dir, + [ + "--backend", + "ensemble", + "--backend", + "python", + "--backend", + "onnxruntime", + "--repoagent", + "checksum", + "--cache", + "local", + "--build-presets-file", + str(preset), + ], + ) + assert proc.returncode == 0, proc.stderr + cmake = read_cmake_build(reload_dir) + assert "TRITON_BOOST_URL=https://example.com/boost.tar.gz" in cmake + assert "-DTRITON_ENABLE_GPU:BOOL=ON" in cmake # pinned core flag + + +def test_cli_tag_wins_over_preset(tmp_path): + """Precedence: an explicit CLI '--backend onnxruntime:from_cli' tag wins over + the preset file's tag.""" + preset = tmp_path / "preset.json" + preset.write_text(json.dumps({"backends": {"onnxruntime": {"tag": "from_preset"}}})) + proc = run_build( + tmp_path, + [ + "--backend", + "ensemble", + "--backend", + "onnxruntime:from_cli", + "--build-presets-file", + str(preset), + ], + ) + assert proc.returncode == 0, proc.stderr + cmake = read_cmake_build(tmp_path) + assert "-b from_cli " in cmake + assert "from_preset" not in cmake + + +def test_backend_not_in_build_is_rejected(tmp_path): + """Validation: a preset naming a backend not included in the build fails with + a clear 'not included in the build' error.""" + preset = tmp_path / "preset.json" + preset.write_text(json.dumps({"backends": {"nope": {"tag": "x"}}})) + proc = run_build( + tmp_path, ["--backend", "ensemble", "--build-presets-file", str(preset)] + ) + assert proc.returncode != 0 + assert "not included in the build" in proc.stderr + + +def test_unknown_component_key_is_rejected(tmp_path): + """Validation: a preset with an unknown per-component key (typo) fails with a + clear 'unknown key' error.""" + preset = tmp_path / "preset.json" + preset.write_text(json.dumps({"backends": {"onnxruntime": {"taag": "x"}}})) + proc = run_build( + tmp_path, + [ + "--backend", + "ensemble", + "--backend", + "onnxruntime", + "--build-presets-file", + str(preset), + ], + ) + assert proc.returncode != 0 + assert "unknown key" in proc.stderr + + +if __name__ == "__main__": + # Self-sufficient standalone entry (`python3 build_presets_test.py`): install + # this suite's deps (pytest + build.py's distro/requests), then run the tests. + # build.py itself is located/cloned by server_dir(). Set L2_NO_BOOTSTRAP=1 to + # skip the install (e.g. deps already present). + if not os.environ.get("L2_NO_BOOTSTRAP"): + subprocess.run( + [ + sys.executable, + "-m", + "pip", + "install", + "-q", + "-r", + str(Path(__file__).with_name("requirements.txt")), + ] + ) + import pytest + + raise SystemExit(pytest.main([__file__])) diff --git a/qa/L2_build_presets/requirements.txt b/qa/L2_build_presets/requirements.txt new file mode 100644 index 0000000000..564d93af84 --- /dev/null +++ b/qa/L2_build_presets/requirements.txt @@ -0,0 +1,3 @@ +distro +pytest +requests diff --git a/qa/L2_build_presets/test.sh b/qa/L2_build_presets/test.sh new file mode 100755 index 0000000000..329c100aea --- /dev/null +++ b/qa/L2_build_presets/test.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause + +# Validate the documented build-presets scenarios via build.py --dryrun (no GPU, +# container, or real build). Runs from the server source tree; see README.md. + +TEST_LOG="build_presets_test.log" +TEST_REPORT="build_presets_test.report.xml" +RET=0 +python3 -m pip install --quiet -r requirements.txt >/dev/null 2>&1 || true + +# tee: stream to the console AND persist a full log for CI archiving. +# --junitxml= writes machine-readable results (NOTE: the '=' is required; +# 'pytest --junitxml FILE.py' would treat FILE.py as the output path). +# PIPESTATUS[0] captures pytest's exit code, not tee's. +python3 -m pytest -v --log-cli-level=INFO --junitxml="$TEST_REPORT" build_presets_test.py 2>&1 | tee "$TEST_LOG" +RET=${PIPESTATUS[0]} + +if [ $RET -eq 0 ]; then + echo -e "\n***\n*** Test Passed\n***" +else + echo -e "\n***\n*** Test FAILED\n***" +fi +exit $RET diff --git a/tools/add_spdx_header.py b/tools/add_spdx_header.py new file mode 100644 index 0000000000..5ce5badd90 --- /dev/null +++ b/tools/add_spdx_header.py @@ -0,0 +1,156 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause + +"""Insert / maintain / migrate to an SPDX license header on source files. + +Pre-commit hook that adopts the two-line SPDX header on every file it is run +against. Because pre-commit runs hooks on the files staged in a commit, scoping +this hook by file type (rather than by directory) migrates each source file to +SPDX *the first time it is touched* -- a low-risk, incremental rollout. + +The header uses a copyright *year range*, mirroring the repo convention (the +``LICENSE`` file and ``add_copyright.py`` use ``-``):: + + # SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: BSD-3-Clause + +Per file, the hook does exactly one of: + +* **Maintain** -- already SPDX: bump the copyright end year to the current year. +* **Migrate** -- carries the legacy long-form NVIDIA BSD header: replace that + whole block in place with the two SPDX lines, preserving the comment style + (``#`` or ``//``), the shebang, and the block's start year. +* **Insert** -- no NVIDIA header: add the SPDX header (after a shebang if any). + +Idempotent, and coexists with ``add_copyright.py`` (which runs first and only +maintains the copyright year on the legacy/SPDX string it recognizes). +""" + +import os +import re +import sys +from datetime import datetime + +ROOT_DIR = os.path.join(os.path.dirname(__file__), os.pardir) +LICENSE_PATH = os.path.join(ROOT_DIR, "LICENSE") +CURRENT_YEAR = str(datetime.now().year) + +_SPDX_MARKER = "SPDX-License-Identifier" +_LICENSE_ID = "BSD-3-Clause" + +# Start year of the project-wide LICENSE (used only when inserting into a file +# that has no NVIDIA copyright at all). +_LICENSE_YEAR_PAT = re.compile(r"Copyright \(c\) (\d{4})(?:-\d{4})?, NVIDIA") +# Existing SPDX copyright line (for year maintenance). +_SPDX_YEAR_PAT = re.compile( + r"(SPDX-FileCopyrightText: Copyright \(c\) )(\d{4})(?:-\d{4})?(, NVIDIA)" +) +# First line of a legacy long-form NVIDIA BSD header (captures comment prefix + +# start year). The block ends at the standard BSD "SUCH DAMAGE" line. +_LEGACY_COPY_PAT = re.compile( + r"^(#|//) ?Copyright(?: \(c\))? (\d{4})(?:-\d{4})?,? NVIDIA CORPORATION" +) +_LEGACY_END_MARK = "POSSIBILITY OF SUCH DAMAGE" + +_CPP_EXTS = (".cc", ".cpp", ".cxx", ".h", ".hpp", ".cu", ".cuh") + + +def _year_range(start): + return start if start == CURRENT_YEAR else "{}-{}".format(start, CURRENT_YEAR) + + +def _license_start_year(): + try: + with open(LICENSE_PATH, "r", encoding="utf-8") as f: + match = _LICENSE_YEAR_PAT.search(f.read()) + if match: + return match.group(1) + except OSError: + pass + return CURRENT_YEAR + + +def _spdx_lines(prefix, years): + return ( + "{p} SPDX-FileCopyrightText: Copyright (c) {y}, NVIDIA CORPORATION " + "& AFFILIATES. All rights reserved.\n" + "{p} SPDX-License-Identifier: {lic}\n".format( + p=prefix, y=years, lic=_LICENSE_ID + ) + ) + + +def _maintain(content): + def _bump(match): + return match.group(1) + _year_range(match.group(2)) + match.group(3) + + return _SPDX_YEAR_PAT.sub(_bump, content) + + +def _migrate_legacy(content): + """Replace a legacy long-form NVIDIA BSD header with the SPDX lines. Returns + the new content, or None if no legacy header is found.""" + lines = content.splitlines(keepends=True) + start = prefix = start_year = None + for i, line in enumerate(lines): + match = _LEGACY_COPY_PAT.match(line) + if match: + start, prefix, start_year = i, match.group(1), match.group(2) + break + if start is None: + return None + end = None + for j in range(start, len(lines)): + if _LEGACY_END_MARK in lines[j]: + end = j + break + if end is None: + return None + header = _spdx_lines(prefix, _year_range(start_year)) + return "".join(lines[:start]) + header + "".join(lines[end + 1 :]) + + +def _insert(path, content): + prefix = "//" if path.endswith(_CPP_EXTS) else "#" + header = _spdx_lines(prefix, _year_range(_license_start_year())) + "\n" + lines = content.splitlines(keepends=True) + if lines and lines[0].startswith("#!"): + return lines[0] + header + "".join(lines[1:]) + return header + content + + +def process(path): + """Bring ``path`` to an SPDX header (maintain/migrate/insert). Returns True if + the file was modified.""" + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + except (OSError, UnicodeDecodeError): + return False + + if _SPDX_MARKER in content: + updated = _maintain(content) + else: + updated = _migrate_legacy(content) + if updated is None: + updated = _insert(path, content) + + if updated == content: + return False + with open(path, "w", encoding="utf-8") as f: + f.write(updated) + return True + + +def main(argv): + changed = False + for path in argv[1:]: + if process(path): + print("Updated SPDX header: {}".format(path)) + changed = True + # Non-zero exit tells pre-commit the file was modified so it re-stages. + return 1 if changed else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tools/build/build_presets.py b/tools/build/build_presets.py index fe7c87a87d..1e7788feee 100644 --- a/tools/build/build_presets.py +++ b/tools/build/build_presets.py @@ -1,28 +1,5 @@ -# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of NVIDIA CORPORATION nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause """Experimental build presets for build.py.