diff --git a/.github/pre-commit/spelling_allowlist.txt b/.github/pre-commit/spelling_allowlist.txt index 52dd6d38eda..3f55d5cffb3 100644 --- a/.github/pre-commit/spelling_allowlist.txt +++ b/.github/pre-commit/spelling_allowlist.txt @@ -368,6 +368,7 @@ toolchains toolset transmon transpile +trixie trotterization uccsd unary @@ -381,6 +382,7 @@ unmarshal unmarshalling unoptimized upvote +url variadic variational vazirani diff --git a/.github/workflows/build_package_sources.yml b/.github/workflows/build_package_sources.yml index de34e796a64..f2a89829b73 100644 --- a/.github/workflows/build_package_sources.yml +++ b/.github/workflows/build_package_sources.yml @@ -322,6 +322,38 @@ jobs: fi ls -la "$d"/*trimmed* 2>/dev/null || true + - name: Generate pip attribution (NOTICE_PIP per variant + CUDA version) + run: | + cuda_major=$(echo "${{ matrix.cuda }}" | cut -d. -f1) + suffix="_cu${cuda_major}" + python3 scripts/generate_pip_attribution.py \ + package-source-diff/pip_packages_cudaq.txt \ + -o "NOTICE_PIP_cudaq${suffix}" + + python3 scripts/generate_pip_attribution.py \ + package-source-diff/pip_packages_cudaqx.txt \ + -o "NOTICE_PIP_cudaqx${suffix}" + + python3 scripts/generate_pip_attribution.py \ + package-source-diff/pip_packages_macos.txt \ + -o "NOTICE_PIP_macos${suffix}" + ls -la NOTICE_PIP_* + head -50 "NOTICE_PIP_cudaq${suffix}" + + - name: Generate apt attribution (NOTICE_APT per variant + CUDA version) + run: | + cuda_major=$(echo "${{ matrix.cuda }}" | cut -d. -f1) + suffix="_cu${cuda_major}" + python3 scripts/generate_apt_attribution.py \ + package-source-diff/apt_packages_cudaq.txt \ + -o "NOTICE_APT_cudaq${suffix}" + + python3 scripts/generate_apt_attribution.py \ + package-source-diff/apt_packages_cudaqx.txt \ + -o "NOTICE_APT_cudaqx${suffix}" + ls -la NOTICE_APT_* + head -50 "NOTICE_APT_cudaq${suffix}" + - name: Generate tpls lock file run: | chmod +x scripts/generate_tpls_lock.sh diff --git a/NOTICE b/NOTICE index 224b4d24875..dd881c3ffef 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,7 @@ CUDA-Q +Copyright (c) 2022-2026 NVIDIA Corporation & Affiliates. + This product includes software developed by NVIDIA corporation and affiliates and includes material from third parties. It includes work released under the following licenses: ---------------------------------------------------------------- @@ -7,8 +9,9 @@ This product includes software developed by NVIDIA corporation and affiliates an LLVM - Apache License 2.0 with LLVM Exceptions and -Originally developed by University of Illinois at Urbana Campaign. -The incorporated source code and its license can be found as as submodule on the CUDA-Q repository. +Copyright (c) 2003-2019 University of Illinois at Urbana-Champaign. All rights reserved. +Originally developed by University of Illinois at Urbana-Champaign. +The incorporated source code and its license can be found as a submodule on the CUDA-Q repository. License at ---------------------------------------------------------------- @@ -24,23 +27,30 @@ See also . PyBind11 - BSD-style license +Copyright (c) 2016 Wenzel Jakob and pybind11 contributors. The source code is based on the work originally developed by Wenzel Jakob License at ---------------------------------------------------------------- -Ensmallen - 3-Clause BSD License - +Ensmallen +https://github.com/mlpack/ensmallen +Copyright (c) 2011–2018, ensmallen contributors + +This software is licensed under the BSD 3-Clause License. The incorporated source code and its license can be found as a submodule on the CUDA-Q repository. -License at +https://github.com/mlpack/ensmallen/blob/master/LICENSE.txt ---------------------------------------------------------------- Armadillo - Apache License 2.0 -The incorporated source code and its license can be found as as submodule on the CUDA-Q repository. +Copyright 2008-2023 Conrad Sanderson (https://conradsanderson.id.au) +Copyright 2008-2016 National ICT Australia (NICTA) +Copyright 2017-2023 Data61 / CSIRO +The incorporated source code and its license can be found as a submodule on the CUDA-Q repository. License at ---------------------------------------------------------------- @@ -48,6 +58,7 @@ License at +Copyright (c) 2013-2026 Niels Lohmann. The source code is based on the work originally developed by Niels Lohmann License at @@ -56,23 +67,26 @@ License at Quantum++ - MIT License +Copyright (c) 2017-2025 SoftwareQ Inc. The source code is based on the work originally developed by SoftwareQ Inc -License at +License at ---------------------------------------------------------------- {fmt} - MIT License +Copyright (c) 2012-present Victor Zverovich and {fmt} contributors. Originally developed by Victor Zverovich and contributors. The incorporated source code and its license can be found as a submodule on the CUDA-Q repository. -License at +License at ---------------------------------------------------------------- SPDLog - MIT License +Copyright (c) 2016-present Gabi Melman and spdlog contributors. Originally developed by Gabi Melman. The incorporated source code and its license can be found as a submodule on the CUDA-Q repository. License at @@ -91,6 +105,7 @@ See also ExaTN - 3-Clause BSD License +Copyright (c) 2017, UT-Battelle License at ---------------------------------------------------------------- @@ -98,6 +113,7 @@ License at CuQuantum - 3-Clause BSD License +Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. License at ---------------------------------------------------------------- @@ -105,7 +121,8 @@ License at C++ Requests - MIT License -Originally developed by Huu Nguyen, libcpr and contributors. +Copyright (c) 2017-2021 Huu Nguyen +Copyright (c) 2022 libcpr and many other contributors License at ---------------------------------------------------------------- @@ -120,6 +137,8 @@ See also XTL - BSD-3-Clause +Copyright (c) 2016, Johan Mabille, Sylvain Corlay and Wolf Vollprecht +Copyright (c) 2016, QuantStack License at ---------------------------------------------------------------- @@ -127,6 +146,8 @@ License at XTensor - BSD-3-Clause +Copyright (c) 2016, Johan Mabille, Sylvain Corlay and Wolf Vollprecht +Copyright (c) 2016, QuantStack License at ---------------------------------------------------------------- @@ -134,6 +155,7 @@ License at zlib - custom license +(C) 1995-2026 Jean-loup Gailly and Mark Adler License at ---------------------------------------------------------------- @@ -141,6 +163,8 @@ License at OpenSSL - Apache License 2.0 +Copyright (c) 1998-2025 The OpenSSL Project Authors +Copyright (c) 1995-1998 Eric A. Young, Tim J. Hudson Licenses at ---------------------------------------------------------------- @@ -148,6 +172,8 @@ Licenses at Curl - custom license +Copyright (c) 1996 - 2026, Daniel Stenberg, , and many +contributors, see the THANKS file. Licenses at ---------------------------------------------------------------- @@ -155,13 +181,16 @@ Licenses at ASIO - Boost Software License - Version 1.0 -License at +Copyright (c) 2003-2026 Christopher M. Kohlhoff (chris at kohlhoff dot com) +License at ---------------------------------------------------------------- Crow - BSD-3-Clause +Copyright (c) 2014-2017, ipkn + 2020-2022, CrowCpp License at ---------------------------------------------------------------- @@ -169,6 +198,8 @@ License at Tweedledum - MIT License +Copyright (c) 2018 - Present, EPFL +Copyright (c) 2018 - Present, Bruno Schmitt License at ---------------------------------------------------------------- @@ -176,6 +207,7 @@ License at Stim - Apache License 2.0 +Copyright 2025 Google LLC. The incorporated source code and its license can be found as a submodule on the CUDA-Q repository. License at diff --git a/docker/build/package_sources.Dockerfile b/docker/build/package_sources.Dockerfile index e2824566b4a..2fc333fc52c 100644 --- a/docker/build/package_sources.Dockerfile +++ b/docker/build/package_sources.Dockerfile @@ -29,7 +29,9 @@ # tpls_commits.lock - " " per submodule (same as install_prerequisites.sh -l) # .gitmodules - submodule paths and URLs # scripts/clone_tpls_from_lock.sh - clone script -# NOTICE, LICENSE - attribution +# NOTICE, LICENSE - attribution +# NOTICE_PIP_cudaq_cu, NOTICE_PIP_cudaqx_cu, NOTICE_PIP_macos_cu - pip attribution per variant (from generate_pip_attribution.py) +# NOTICE_APT_cudaq_cu, NOTICE_APT_cudaqx_cu - apt attribution per variant (from generate_apt_attribution.py) ARG base_image=ubuntu:24.04 FROM ${base_image} @@ -67,7 +69,8 @@ RUN if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then \ RUN apt-get update ENV SOURCES_ROOT=/sources -RUN mkdir -p "${SOURCES_ROOT}/cudaq/apt" \ +RUN mkdir -p "${SOURCES_ROOT}/NOTICES" \ + "${SOURCES_ROOT}/cudaq/apt" \ "${SOURCES_ROOT}/cudaq/pip" \ "${SOURCES_ROOT}/cudaqx/apt" \ "${SOURCES_ROOT}/cudaqx/pip" \ @@ -84,8 +87,10 @@ COPY scripts/clone_tpls_from_lock.sh "${SCRIPTS_DIR}"/clone_tpls_from_lock.sh COPY package-source-diff/apt_packages_cudaq.txt package-source-diff/apt_packages_cudaqx.txt package-source-diff/apt_packages_cudaqx_trimmed.txt "${SCRIPTS_DIR}"/ COPY package-source-diff/pip_packages_cudaq.txt package-source-diff/pip_packages_cudaqx.txt package-source-diff/pip_packages_cudaqx_trimmed.txt package-source-diff/pip_packages_macos.txt package-source-diff/pip_packages_macos_trimmed.txt "${SCRIPTS_DIR}"/ -# Copy attribution -COPY NOTICE LICENSE "${SOURCES_ROOT}/" +# Copy attribution into NOTICES folder (NOTICE_PIP_* and NOTICE_APT_* generated per variant with CUDA version, e.g. NOTICE_PIP_cudaq_cu12) +COPY NOTICE LICENSE "${SOURCES_ROOT}/NOTICES/" +COPY NOTICE_PIP_cudaq_cu* NOTICE_PIP_cudaqx_cu* NOTICE_PIP_macos_cu* "${SOURCES_ROOT}/NOTICES/" +COPY NOTICE_APT_cudaq_cu* NOTICE_APT_cudaqx_cu* "${SOURCES_ROOT}/NOTICES/" # Fetch apt source, pip sdists, and clone tpls in parallel (prefix lines so logs stay readable) RUN apt-get update && set -o pipefail && \ diff --git a/scripts/generate_apt_attribution.py b/scripts/generate_apt_attribution.py new file mode 100644 index 00000000000..5cc2527abd2 --- /dev/null +++ b/scripts/generate_apt_attribution.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# ============================================================================ # +# Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # +# +# Reads apt package lists (one package name per line), optionally fetches +# Debian source metadata for each package, and writes a NOTICE-style attribution +# file with references to license and copyright (in-image path and upstream). + +import argparse +import json +import re +import sys +import urllib.parse +import urllib.request +from collections import OrderedDict + +DEBIAN_SRC_API = "https://sources.debian.org/api/src/{name}/" +DEBIAN_LICENSE_PAGE = "https://sources.debian.org/copyright/license/{name}/{version}/" +# First 4-digit year or year range in copyright context (e.g. 1994, 1996-2022) +COPYRIGHT_YEAR_RE = re.compile(r"\b(19|20)\d{2}(?:-(19|20)\d{2})?\b") + +HEADER = """Apt packages - third-party attribution +This file lists license and copyright information for apt packages included +in the CUDA-Q package sources image. Full text is in /usr/share/doc//copyright +in the image; links below point to Debian or Ubuntu package pages. + +""" + + +def fetch_copyright_year(name: str, version: str) -> str | None: + """Fetch Debian license page and return first copyright year or year range, or None.""" + url = DEBIAN_LICENSE_PAGE.format( + name=urllib.parse.quote(name, safe=""), + version=urllib.parse.quote(version, safe=""), + ) + try: + req = urllib.request.Request( + url, + headers={"Accept": "text/html,application/xhtml+xml"}, + ) + with urllib.request.urlopen(req, timeout=10) as resp: + body = resp.read().decode("utf-8", errors="replace") + except Exception: + return None + # Restrict to content before "Licenses" section to avoid matching license text + if "## Licenses" in body: + body = body.split("## Licenses")[0] + m = COPYRIGHT_YEAR_RE.search(body) + if m: + return m.group(0) + return None + + +def fetch_debian_versions(name: str) -> str | None: + """Fetch Debian source package versions. Returns one version string or None.""" + url = DEBIAN_SRC_API.format(name=urllib.parse.quote(name, safe="")) + try: + req = urllib.request.Request(url, + headers={"Accept": "application/json"}) + with urllib.request.urlopen(req, timeout=10) as resp: + data = json.load(resp) + except Exception: + return None + versions = data.get("versions") or [] + # Prefer a stable suite (bookworm, trixie) then take first + for v in versions: + if v.get("suites") and any(s in ("bookworm", "trixie", "sid") + for s in v.get("suites", [])): + return v.get("version") + return versions[0].get("version") if versions else None + + +def format_entry( + name: str, + version: str | None, + copyright_year: str | None = None, +) -> str: + """Format a single NOTICE-style entry for one package.""" + doc_path = f"/usr/share/doc/{name}/copyright" + lines = [ + f"{name} - See {doc_path} in the image.", + "", + ] + if version: + license_url = ( + f"https://sources.debian.org/copyright/license/{name}/{version}/") + lines.append(f"<{license_url}>") + else: + lines.append("") + lines.append("") + if copyright_year: + lines.append(f"Copyright (c) {copyright_year}.") + lines.append("") + lines.append( + "----------------------------------------------------------------") + lines.append("") + return "\n".join(lines) + + +def load_package_names(paths: list[str]) -> OrderedDict[str, None]: + """Load unique package names from one or more apt list files (one name per line).""" + seen: OrderedDict[str, None] = OrderedDict() + for path in paths: + try: + with open(path) as f: + for line in f: + name = line.strip() + if not name or name.startswith("#"): + continue + seen[name] = None + except FileNotFoundError: + continue + return seen + + +def main(): + parser = argparse.ArgumentParser( + description="Generate NOTICE-style attribution file for apt packages.") + parser.add_argument( + "list_files", + nargs="+", + metavar="apt_list.txt", + help="One or more files with one package name per line", + ) + parser.add_argument( + "-o", + "--output", + default="NOTICE_APT", + help="Output file path (default: NOTICE_APT)", + ) + parser.add_argument( + "--no-header", + action="store_true", + help="Omit the header comment", + ) + parser.add_argument( + "--no-fetch", + action="store_true", + help="Do not fetch Debian API; use Ubuntu link only", + ) + parser.add_argument( + "--no-copyright-year", + action="store_true", + help="Do not fetch copyright year from Debian license page", + ) + args = parser.parse_args() + + packages = load_package_names(args.list_files) + if not packages: + sys.stderr.write("No packages found in list files.\n") + sys.exit(1) + + out_lines = [] if args.no_header else [HEADER] + + for name in packages: + version = None + copyright_year = None + if not args.no_fetch: + version = fetch_debian_versions(name) + if version and not args.no_copyright_year: + copyright_year = fetch_copyright_year(name, version) + out_lines.append(format_entry(name, version, copyright_year)) + + with open(args.output, "w") as f: + f.write("".join(out_lines)) + + print( + f"Wrote {args.output} with {len(packages)} packages.", + file=sys.stderr, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_pip_attribution.py b/scripts/generate_pip_attribution.py new file mode 100644 index 00000000000..8c52f85d948 --- /dev/null +++ b/scripts/generate_pip_attribution.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# ============================================================================ # +# Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # +# +# Reads pip package lists (name==version per line), fetches PyPI metadata for +# each package, and writes a NOTICE-style attribution file with license and +# copyright year(s). + +import argparse +import json +import re +import sys +import urllib.request +from collections import OrderedDict + +PYPI_URL = "https://pypi.org/pypi/{name}/{version}/json" +HEADER = """Pip packages - third-party attribution +This file lists license and copyright information for pip packages included +in the CUDA-Q package sources image. Data is derived from PyPI metadata. + +""" + + +def fetch_pypi_metadata(name: str, version: str) -> dict | None: + """Fetch PyPI JSON for the given package and version. Returns None on failure.""" + url = PYPI_URL.format(name=name, version=version) + try: + req = urllib.request.Request(url, + headers={"Accept": "application/json"}) + with urllib.request.urlopen(req, timeout=15) as resp: + return json.load(resp) + except Exception: + return None + + +def get_license_from_classifiers(classifiers: list[str] | None) -> str | None: + """Extract license from Trove classifiers (e.g. 'License :: OSI Approved :: MIT').""" + if not classifiers: + return None + for c in classifiers: + if c.startswith("License ::"): + # Return the last part, e.g. "MIT License" from "License :: OSI Approved :: MIT License" + parts = c.split(" :: ") + return parts[-1] if len(parts) >= 2 else c + return None + + +def get_copyright_year(data: dict) -> str | None: + """Get copyright year from release upload_time (this version).""" + urls = data.get("urls") or [] + for u in urls: + t = u.get("upload_time") or u.get("upload_time_iso_8601") + if t: + # "2025-03-05" or "2025-03-05T20:05:00" + year = t[:4] + if year.isdigit(): + return year + return None + + +def get_author(data: dict) -> str | None: + """Get author or maintainer for copyright line.""" + info = data.get("info") or {} + author = (info.get("author") or "").strip() + if author: + return author + maintainer = (info.get("maintainer") or "").strip() + if maintainer: + return maintainer + return None + + +def get_project_url(data: dict) -> str | None: + """Get project URL (home_page or package_url).""" + info = data.get("info") or {} + return info.get("home_page") or info.get("package_url") or info.get( + "project_url") + + +def format_entry(name: str, version: str, data: dict) -> str: + """Format a single NOTICE-style entry for one package.""" + info = data.get("info") or {} + license_ = (info.get("license") or + "").strip() or get_license_from_classifiers( + info.get("classifiers")) + if not license_: + license_ = "See project for license." + year = get_copyright_year(data) + author = get_author(data) + url = get_project_url(data) + + lines = [ + f"{name} {version} - {license_}", + "", + ] + if url: + lines.append(f"<{url}>") + lines.append("") + if year and author: + lines.append(f"Copyright (c) {year} {author}.") + elif year: + lines.append(f"Copyright (c) {year}.") + elif author: + lines.append(f"Copyright (c) {author}.") + if lines[-1]: + lines.append("") + lines.append( + "----------------------------------------------------------------") + lines.append("") + return "\n".join(lines) + + +def load_package_versions(paths: list[str]) -> OrderedDict[str, str]: + """Load name -> version from one or more pip list files (name==version). Keeps first seen version.""" + seen = OrderedDict() + for path in paths: + try: + with open(path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + if "==" in line: + name, _, version = line.partition("==") + name = name.strip() + version = version.strip() + if name and version: + seen.setdefault(name, version) + except FileNotFoundError: + continue + return seen + + +def main(): + parser = argparse.ArgumentParser( + description= + "Generate NOTICE-style attribution file for pip packages from PyPI metadata." + ) + parser.add_argument( + "list_files", + nargs="+", + metavar="pip_list.txt", + help="One or more files with name==version per line", + ) + parser.add_argument( + "-o", + "--output", + default="NOTICE_PIP", + help="Output file path (default: NOTICE_PIP)", + ) + parser.add_argument( + "--no-header", + action="store_true", + help="Omit the header comment", + ) + args = parser.parse_args() + + packages = load_package_versions(args.list_files) + if not packages: + sys.stderr.write("No packages found in list files.\n") + sys.exit(1) + + out_lines = [] if args.no_header else [HEADER] + + for name, version in packages.items(): + data = fetch_pypi_metadata(name, version) + if data: + out_lines.append(format_entry(name, version, data)) + else: + out_lines.append( + f"{name} {version} - (license/copyright not retrieved from PyPI)\n" + f"\n\n" + "----------------------------------------------------------------\n\n" + ) + + with open(args.output, "w") as f: + f.write("".join(out_lines)) + + print(f"Wrote {args.output} with {len(packages)} packages.", + file=sys.stderr) + + +if __name__ == "__main__": + main()