Skip to content

build(cmake): export dependency usage requirements as imported targets - #1157

Open
IvanaGyro wants to merge 10 commits into
masterfrom
claude/issue-1120-ng4p5n
Open

build(cmake): export dependency usage requirements as imported targets#1157
IvanaGyro wants to merge 10 commits into
masterfrom
claude/issue-1120-ng4p5n

Conversation

@IvanaGyro

@IvanaGyro IvanaGyro commented Jul 31, 2026

Copy link
Copy Markdown
Member

Closes #1120.

Main Improvement

This PR enable us to include lapacke.h installed by pixi and allow us to use pixi to lock/provide/maintain/list system dependencies. This will ease a lot of pain on system configuration.

Problem

Cytnx::cytnx propagated its dependencies as absolute paths detected on the build machine:

  • ${LAPACKE_INCLUDE_DIRS} and ${LAPACKE_LIBRARIES} in the OpenBLAS branch of CytnxBKNDCMakeLists.cmake
  • ${LAPACK_LIBRARIES} in the MKL branch
  • ${CUTENSOR_INCLUDE_DIRS} / ${CUQUANTUM_INCLUDE_DIRS} and their libraries under USE_CUTENSOR / USE_CUQUANTUM
  • ${ARPACK_LIB}

install(EXPORT) copies those verbatim into CytnxTargets.cmake, so the installed package only resolved on a machine whose dependencies sit at the same paths.

They also cannot simply be hidden behind a BUILD_INTERFACE generator expression, because they are genuine public usage requirements: the installed header backend/lapack_wrapper.hpp includes lapacke.h, and cytnx_error.hpp / Network.hpp include cutensor.h / cutensornet.h under the PUBLIC UNI_CUTENSOR / UNI_CUQUANTUM definitions. arpack is PRIVATE, but libcytnx is a STATIC archive, so even that reaches the export under LINK_ONLY — the consumer's final link is what resolves its symbols.

Two configure-time mechanisms made this structural rather than incidental, because each selected libraries from a fact about the build machine that no consumer can reproduce:

  • the arpack/OpenBLAS deduplication probe, which ran ldd / otool -L against the installed arpack and overwrote BLAS_LIBRARIES / LAPACK_LIBRARIES / LAPACKE_LIBRARIES with whatever OpenBLAS arpack happened to be linked against;
  • the pinned-library escape hatch, which skipped both providers when the caller predefined those three variables and linked the given paths directly.

The build-policy items from the issue (-w, -Wformat=0, -fsized-deallocation, _LIBCPP_DISABLE_AVAILABILITY made PRIVATE; --coverage as PRIVATE compile + INTERFACE link; Boost::boost owning its own includes) were already correct on master. This PR covers the dependency-path half, and adds CI that exercises the coverage half.

Fix

Remove the two machine-specific mechanisms first, then express every remaining dependency as an imported target the consumer rediscovers.

Both removals are safe because dependency provisioning already guarantees a single OpenBLAS on every platform cytnx ships: manylinux and macOS take arpack and OpenBLAS from the same conda-forge environment pinned to openblas=*=*openmp*, and musllinux keeps Alpine's arpack while building OpenBLAS from source into the soname that arpack's own NEEDED entry names (tools/cibuildwheel_before_all.sh). Nothing that ships pins the three *_LIBRARIES variables either.

Path Before, in the export After
OpenBLAS ${LAPACKE_INCLUDE_DIRS}, ${LAPACKE_LIBRARIES}, ${LAPACK_LIBRARIES} MORSE::LAPACKE + LAPACK::LAPACK
MKL four absolute intel-mkl/lib/*.so paths LAPACK::LAPACK
cuTENSOR / cuQuantum SDK include dirs and library paths CUTENSOR::CUTENSOR / CUQUANTUM::CUQUANTUM
arpack /abs/path/to/libarpack.so ARPACK::ARPACK, from arpack-ng's own package

cmake/Modules/FindCUTENSOR.cmake and FindCUQUANTUM.cmake now create their imported targets.

CytnxConfig.cmake re-runs the matching lookups in the consumer's own environment before including CytnxTargets.cmake. They share a single block(SCOPE_FOR VARIABLES), which holds the one CMAKE_MODULE_PATH prepend and the one BLA_VENDOR restore that all of them need, so neither leaks into the consuming project while the imported targets they create — being directory-scoped — outlive the block. Boost, CUDAToolkit and OpenMP stay outside it, because a consumer legitimately reads the result variables those set and the block would swallow them. The Morse finders are loaded by module path rather than by including Morse's top-level MorseInit.cmake, which would rewrite project-wide options in the consumer. Each SDK lookup is skipped when the consuming project already provides the target, because those finders fatal-error on an unset *_ROOT before they would notice the target exists.

With only two provider branches left, USE_MKL alone tells the generated config which lookup to re-run, so no provider variable is exported.

One finder per provider

Each branch called finders whose work another finder in the same branch already does. CMake's FindLAPACK looks for BLAS itself, and morse_cmake's FindLAPACKE includes FindLAPACKEXT, which calls FindLAPACK — so one find_package(LAPACKE) yields both MORSE::LAPACKE and LAPACK::LAPACK. The MKL branch is one find_package(LAPACK); the OpenBLAS branch is one find_package(LAPACKE). UNI_OPENBLAS is now defined on the target, matching the UNI_MKL the MKL branch already applied.

The LAPACKE lookup has to actually run

Linking MORSE::LAPACKE means the lookup that defines it must not be silently skipped, and CMake's defaults allow two ways for that to happen. Both are avoided explicitly, in the build and in the generated config alike:

  • Config mode winning. The basic find_package signature searches config mode ahead of module mode under CMAKE_FIND_PACKAGE_PREFER_CONFIG. A LAPACKEConfig.cmake that sets the usual variables satisfies the call without defining Morse's target, and the failure then surfaces at generate time as an unknown target rather than at the lookup. Both call sites pass MODULE.
  • find_dependency optimizing the call out. CMake 3.26 restored the pre-3.15 optimization, keyed on a SHA256 of dep;ARGN;required. Another package config in the same directory that already ran find_dependency(LAPACKE MODULE REQUIRED) against its finder makes ours a no-op — and the heuristic cannot see the CMAKE_MODULE_PATH prepend and BLA_VENDOR this block sets first, which is precisely the case its own documentation warns about. The generated config therefore calls find_package(LAPACKE MODULE) directly.

Dropping REQUIRED there would turn a genuine miss into a generate-time error, so the config verifies MORSE::LAPACKE after endblock() and reports a package-not-found reason instead. The check lives outside the block because block(SCOPE_FOR VARIABLES) would swallow Cytnx_FOUND / Cytnx_NOT_FOUND_MESSAGE, while the imported target it tests is directory-scoped and stays visible.

arpack through arpack-ng's package config

find_package(arpackng CONFIG) defines ARPACK::ARPACK, cytnx links that, and the generated config re-finds the package so a consumer binds its own arpack. Both the build and the generated config accept either spelling of the package name, because the platforms cytnx ships do not agree on one: manylinux and macOS take arpack from conda-forge, which carries 3.9's lib/cmake/arpackng/arpackng-config.cmake, while musllinux builds against Alpine's arpack-dev, still on 3.8.0 and shipping usr/lib/cmake/arpack-ng/arpack-ng-config.cmake (upstream renamed the package from arpack-ng to arpackng in 3.9). find_package(arpackng CONFIG QUIET) first, then arpack-ng — in the generated config the first attempt has to be find_package rather than find_dependency, because find_dependency aborts the whole config on a miss even when QUIET. Either way it is arpack-ng's own package config, so cytnx needs no finder module of its own and installs none.

The two releases also differ in the shape of ARPACK::ARPACK, which decides what lands in the export. 3.9 defines it as an alias of a target named arpack, and CMake records the aliased target's name when it writes an export, so CytnxTargets.cmake names arpack. 3.8 defines it as an INTERFACE IMPORTED target in its own right, so the export names ARPACK::ARPACK. Both are target names rather than paths — which is the property this PR is after — and each side's lookup recreates whichever name its own export holds. Verified against both real packages; the CI assertion below is case-insensitive for that reason.

Because that name varies, the build records which one it produced rather than letting the generated config assume:

get_target_property(cytnx_arpack_aliased ARPACK::ARPACK ALIASED_TARGET)

The result is substituted into CytnxConfig.cmake, which skips its arpack lookup only when the consuming project already supplies that exact target — a project that builds arpack-ng as a subproject or gets it from a toolchain satisfies the exported dependency without having a package config installed, and re-running the lookup there would both fail find_package(Cytnx) over a present dependency and collide with the arpack target that project already defined. Keying the guard off the exported name is what keeps a consumer that supplies only the other spelling from skipping a lookup the export still needs.

The lookup runs after the BLAS/LAPACK provider: a statically built arpack's config calls find_dependency(BLAS) / find_dependency(LAPACK) unless those targets already exist, so resolving cytnx's provider first stops arpack from binding the build to a vendor other than BLA_VENDOR names. (conda-forge builds arpack shared, so that branch of its config is inert there.)

One template, configured once per consumable tree

cytnx ships two consumable packages — the install tree, and the build tree via export(PACKAGE Cytnx) — and they locate the finder modules differently. Rather than stage a second copy of Morse's closure into the build directory, the single template is configured twice with the module search paths injected: the build-tree config reads the finders straight out of the source tree, and the installed config reads the copies inside the package, which remain the sole responsibility of morse_install_finds(). Nothing is staged into the build directory and no file list is restated.

BLA_VENDOR steers, it does not override

CytnxConfig.cmake asks for the vendor cytnx was built against, but that only steers a lookup that has not happened yet. Neither finder replaces a target that already exists: FindLAPACK creates LAPACK::LAPACK under if(LAPACK_FOUND AND NOT TARGET LAPACK::LAPACK), and FindBLAS guards BLAS::BLAS the same way, so a target another dependency already created survives untouched.

A pre-existing BLAS::BLAS is enough on its own, even when no LAPACK target exists: FindLAPACK removes BLAS_LIBRARIES from the LAPACK::LAPACK it creates and links that BLAS target in their place, so the consumer's BLAS reaches cytnx's link line through a LAPACK target this config did create. Rather than bind silently — an MKL LP64/ILP64 or OpenBLAS/reference mismatch miscomputes instead of failing — the config warns when either target already exists, naming the ones it found, the expected vendor, and the two ways out. A warning and not an error, because compatibility cannot be decided from inside the config and the pre-existing target is often the same implementation.

Two corrections to the issue's plan

  1. MORSE::LAPACKE alone is not sufficient. The issue and its verification comment state that the target carries the detected libraries, so dropping ${LAPACKE_LIBRARIES} / ${LAPACK_LIBRARIES} is enough. That holds only when LAPACKE is embedded in LAPACK (MKL, some OpenBLAS packagings). With a standalone liblapacke — the layout the CI uses — FindLAPACKE records liblapacke.so alone, and cytnx also calls plain BLAS/LAPACK entry points. Following the issue literally made the downstream consumer fail to link with libblas.so.3: error adding symbols: DSO missing from command line. Hence LAPACK::LAPACK alongside it.

  2. Finding LAPACK first does not fold it into MORSE::LAPACKE. Probed directly: the consumer-side target stays liblapacke.so either way. The ordering is kept because FindLAPACKE requires LAPACK regardless, not because it merges the two.

Consumer-visible constraint

block() puts a CMake >= 3.25 floor on consuming the package, matching cytnx's own build floor. CytnxConfig.cmake reports it as a package-not-found reason rather than failing on an unknown command. cmake_minimum_required() is deliberately not used there — it would apply cytnx's policy settings to the consuming project's scope.

CI

ci-downstream-find-package gains two things:

  • a configuration matrix — uninstrumented (RUN_TESTS=OFF, what a real downstream user builds) and coverage (RUN_TESTS=ON). cytnx is a STATIC archive, so --coverage has to be an unconditional INTERFACE link option: an instrumented libcytnx.a needs the coverage runtime after installation just as much as before. The coverage leg is what proves it — dropping the option from the install interface would leave that consumer with undefined __gcov_* references.
  • an assertion on the installed export itself. The consumer step passes on the runner that produced the package even when an absolute path is baked in, because that path exists there. The new step reads INTERFACE_LINK_LIBRARIES out of the installed CytnxTargets.cmake and fails on any entry that is a filesystem path, and requires arpack to still be named. Verified against install trees built before and after this branch: it reports /usr/lib/x86_64-linux-gnu/libarpack.so and exits 1 on the former, passes on the latter.

ci-gpu_tests.yml gains an arpack provisioning step. It is the one job whose dependencies come from apt rather than conda-forge, and apt's arpack installs the library and headers with no package config, so the configure step failed outright once cytnx started resolving arpack through find_package(arpackng CONFIG). The job now creates a workspace-local prefix holding conda-forge's arpack and points CMAKE_PREFIX_PATH at it. micromamba is fetched as a single static binary into the runner's temp dir rather than installing a conda distribution, so nothing outside the workspace changes on the self-hosted machine and the job keeps the plain non-login shell its other steps use.

Testing

Every acceptance criterion was exercised, not assumed. Built and consumed locally with GCC 13 / CMake 3.28 using tests/downstream_find_package, against both the build tree and the install tree:

Check Result
debug-openblas-cpu test_main + ctest 1839/1839 passed, 0 failed
debug-mkl-cpu test_main + ctest 1873/1873 passed, 0 failed
OpenBLAS consumers, build tree + install tree configure, compile, link, run
MKL consumers, both trees configure, compile, link, run; zero intel-mkl paths in the export
Installed CytnxTargets.cmake Boost::boost;MORSE::LAPACKE;LAPACK::LAPACK plus a LINK_ONLY entry naming arpack — no dependency paths, no source-tree paths
arpack against conda-forge's arpack=3.9.1=nompi_* package the package ships lib/cmake/arpackng/arpackng-config.cmake; configuring against it resolves ARPACK::ARPACK, the export names arpack, and both consumers link and run
arpack against Alpine's arpack-dev=3.8.0-r3 package (musllinux's arpack) downloaded the real .apk: it ships usr/lib/cmake/arpack-ng/arpack-ng-config.cmake and defines ARPACK::ARPACK as an INTERFACE IMPORTED target, so the fallback name resolves and the export names ARPACK::ARPACK; a consumer configures against that export
Export name across both packagings built the export under each spelling and configured each against both packages, reading the generated link.txt: the name recorded differs by packaging, and each side's lookup resolves its own
Coverage (RUN_TESTS=ON) consumers, both trees link and run; --coverage in the installed INTERFACE_LINK_OPTIONS and on the install-tree consumer's link line
backend/lapack_wrapper.hpp with LAPACKE under a non-system prefix compiled against the relocated header, proved with a marker macro plus an #error guard; the prefix's include dir (as -isystem) and its liblapacke.so both reached the consumer
Two-config generation build tree stages no finder copies; build-tree config points into the source tree, installed config at the package; installed bundle is the nine-file Morse closure plus the two SDK finders, all under the Development component
Pre-existing CUTENSOR::CUTENSOR / CUQUANTUM::CUQUANTUM reproduced the fatal error at FindCUTENSOR.cmake:25 with *_ROOT unset; guarded form reuses the targets and reaches the end
Vendor-clash warning, pre-existing LAPACK::LAPACK fires on a consumer that resolves reference LAPACK before find_package(Cytnx); silent on the normal path
LAPACKE lookup under CMAKE_FIND_PACKAGE_PREFER_CONFIG with a LAPACKEConfig.cmake that defines no target, the basic signature leaves MORSE::LAPACKE missing while MODULE creates it — the reason both call sites pass MODULE
LAPACKE lookup behind another package's identical find_dependency reproduced the 3.26 call-hash skip: with another config resolving LAPACKE through its own finder first, ours was optimized out and MORSE::LAPACKE never appeared; find_package(LAPACKE MODULE) creates it in both orderings
LAPACKE genuinely unfindable Cytnx_FOUND=0 with a package-not-found reason, rather than a generate-time unknown-target error
arpack guard against the exported name build reports arpack, CytnxTargets.cmake carries a LINK_ONLY:arpack entry, generated config reads if(NOT TARGET arpack); a consumer predefining arpack + alias skips the lookup and configures, one predefining only ARPACK::ARPACK still runs it and fails loudly
Vendor-clash warning, pre-existing BLAS::BLAS only reproduced the silent bind — a consumer resolving reference BLAS first left an OpenBLAS-built Cytnx with LAPACK::LAPACK -> BLAS::BLAS -> libblas.so and no warning; the guard now fires, the clean consumer stays silent, and neither helper variable escapes the block()
CUDA build (CUDA 12.0, cuTENSOR + cuQuantum) configure and export clean — zero SDK paths, UNI_GPU;UNI_CUTENSOR;UNI_CUQUANTUM and cxx_std_20 still propagate; representative .cpp and .cu TUs compile
No leaked build policy -w, -Wformat=0, -fsized-deallocation, _LIBCPP_DISABLE_AVAILABILITY all absent from the consumer's compile line; -std=gnu++20 inherited
GPU job's provisioning step rehearsed end to end: the curl/tar fetch yields a working micromamba, micromamba create produces the arpackng config, and configuring with CMAKE_PREFIX_PATH set to that prefix resolves ARPACK::ARPACK

Known gap, deliberately left

MKL's mkl.h include dir is still not propagated. master never propagated it either (CI and local MKL builds rely on it being on the default include path, e.g. via CPATH), so this is pre-existing rather than a regression from this PR. Point 6 of the issue would be fully closed by modelling MKL through MKL::MKL from MKLConfig.cmake, which is a larger change.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d37e9b1ea5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CytnxBKNDCMakeLists.cmake Outdated
Comment thread cmake/CytnxConfig.cmake.in Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5cc39bc15

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CytnxBKNDCMakeLists.cmake Outdated
Comment thread cmake/CytnxConfig.cmake.in Outdated
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.92%. Comparing base (ba5eb90) to head (1a93594).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1157   +/-   ##
=======================================
  Coverage   67.92%   67.92%           
=======================================
  Files         208      208           
  Lines       22354    22354           
  Branches       72       72           
=======================================
  Hits        15184    15184           
  Misses       7148     7148           
  Partials       22       22           
Flag Coverage Δ
cpp 67.99% <ø> (ø)
python 63.96% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 67.99% <ø> (ø)
Python bindings ∅ <ø> (∅)
Python package 63.96% <ø> (ø)

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ba5eb90...1a93594. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@IvanaGyro
IvanaGyro marked this pull request as draft July 31, 2026 07:55
@IvanaGyro
IvanaGyro marked this pull request as ready for review July 31, 2026 10:57
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@IvanaGyro
IvanaGyro force-pushed the claude/issue-1120-ng4p5n branch from cd35277 to 20995b3 Compare July 31, 2026 18:51
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@IvanaGyro
IvanaGyro marked this pull request as draft July 31, 2026 18:57
@IvanaGyro IvanaGyro mentioned this pull request Aug 2, 2026
IvanaGyro and others added 3 commits August 4, 2026 15:45
The OpenBLAS branch of CytnxBKNDCMakeLists.cmake ran `ldd` (or `otool -L` on
macOS) against the arpack library it had just found, looked for a libopenblas
among arpack's own dependencies, and -- when that differed from what
find_package(BLAS) had resolved, and exported LAPACKE symbols -- overwrote
BLAS_LIBRARIES, LAPACK_LIBRARIES and LAPACKE_LIBRARIES with it. The point was to
stop a wheel from vendoring two OpenBLAS builds on distributions that install a
serial and a threaded build side by side.

Dependency provisioning now guarantees a single OpenBLAS on every platform cytnx
ships, so the probe has nothing left to correct:

* manylinux and macOS take arpack and OpenBLAS from the same conda-forge
  environment (tools/cibuildwheel_before_all.sh,
  tools/cibuildwheel_before_all_macos.sh), pinned to "openblas=*=*openmp*", so
  the arpack that is found is linked against the OpenBLAS that is found.
* musllinux keeps Alpine's arpack and builds OpenBLAS from source with
  USE_OPENMP=1, then patchelf's that build to the soname arpack's own NEEDED
  entry names and symlinks it into place, so arpack resolves to the one
  OpenBLAS present.

This also takes a build-machine fact out of the configure step: the probe chose
libraries by inspecting the arpack installed on the machine running cmake, a
choice no other machine can reproduce.

Co-Authored-By: Claude <noreply@anthropic.com>
Predefining BLAS_LIBRARIES, LAPACK_LIBRARIES and LAPACKE_LIBRARIES made
CytnxBKNDCMakeLists.cmake skip both providers and link those paths straight into
the cytnx target, with no finder run at all.

Nothing configures cytnx that way. Every build that ships -- the manylinux,
macOS and musllinux wheels, the conda recipe, and CI -- provisions its BLAS and
LAPACK and then lets the MKL or OpenBLAS branch find them. musllinux, the one
platform that needs a specific OpenBLAS build, gets it by building that OpenBLAS
into the soname the installed arpack already names
(tools/cibuildwheel_before_all.sh) rather than by pinning cmake variables.

The branch is also the one path here that cannot produce a relocatable package:
the caller's absolute paths land in the exported cytnx target, and a consumer
resolving the package on another machine has no way to reproduce them. Remove
it, leaving USE_MKL to select between the two branches that detect what they
link.

Co-Authored-By: Claude <noreply@anthropic.com>
The downstream find_package job built cytnx only with RUN_TESTS=OFF, so
nothing checked that a coverage-instrumented libcytnx.a stays consumable.
cytnx is a STATIC archive: objects compiled with --coverage leave gcov
symbols unresolved until the consumer's final link, which is why cytnx
carries --coverage as an unconditional INTERFACE link option instead of one
wrapped in $<BUILD_INTERFACE:>. Nothing enforced that distinction.

Run the job as a matrix over RUN_TESTS so both the uninstrumented and the
coverage configuration are driven against the build tree and the install
tree. The coverage variant fails if the option is ever dropped from the
install interface, because the install-tree consumer would then link with
undefined __gcov_* references.

gtest/gmock join the dependency list: RUN_TESTS=ON makes
tests/CMakeLists.txt require them at configure time, even though this job
only ever builds the `cytnx` target. The ccache keys gain the configuration
name so the two variants do not evict each other.

Co-Authored-By: Claude <noreply@anthropic.com>
@IvanaGyro
IvanaGyro force-pushed the claude/issue-1120-ng4p5n branch 4 times, most recently from a01daef to a2bc76a Compare August 4, 2026 17:07
@IvanaGyro
IvanaGyro marked this pull request as ready for review August 4, 2026 17:22
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@IvanaGyro
IvanaGyro force-pushed the claude/issue-1120-ng4p5n branch from a2bc76a to 0deaca9 Compare August 4, 2026 19:28

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0deaca9bb4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CytnxBKNDCMakeLists.cmake
if(NOT arpackng_FOUND)
find_package(arpack-ng CONFIG REQUIRED)
endif()
target_link_libraries(cytnx PRIVATE ARPACK::ARPACK)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Export a stable ARPACK target name

When this build resolves arpackng 3.9, ARPACK::ARPACK is an alias of the un-namespaced arpack target, and CMake serializes the aliased name into CytnxTargets.cmake as $<LINK_ONLY:arpack>. If the installed package is then consumed with the older arpack-ng config that only recreates ARPACK::ARPACK (the case this block explicitly tries to support), arpack is not a target and the downstream link falls back to bare -larpack, losing the package prefix and any static dependency interface; consumers in non-system prefixes fail to link even though find_dependency(arpack-ng) succeeded. Export a Cytnx-owned stable dependency target or recreate the un-namespaced arpack target in the fallback path so the installed find_package(Cytnx) export remains usable.

AGENTS.md reference: AGENTS.md:L118-L119

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The de-aliasing is real and I verified it, but the proposed remedy does not change the outcome it predicts, so I have left this as is.

Confirmed from the two packagings themselves rather than from memory. conda-forge's arpackngTargets.cmake has add_library(arpack SHARED IMPORTED) and the config then does add_library(ARPACK::ARPACK ALIAS arpack), so the export records arpack. Alpine's arpack-ng 3.8.0 config instead does add_library(ARPACK::ARPACK INTERFACE IMPORTED) and defines no target named arpack. So the 3.9-built / 3.8-consumed cell really does lose the target, exactly as described.

What it loses it to is the same thing either way. I built a package export under both spellings and configured each against both packagings, reading the generated link.txt:

consumer has export records arpack export records ARPACK::ARPACK
arpack-ng 3.8.0 (Alpine), non-system prefix -larpack -larpack
arpackng 3.9.1 (conda-forge) -Wl,-rpath,<prefix>/lib <prefix>/lib/libarpack.so.2.1.0 -Wl,-rpath,<prefix>/lib <prefix>/lib/libarpack.so.2.1.0

The reason is in 3.8's config: its ARPACK::ARPACK carries INTERFACE_LINK_LIBRARIES "arpack" — a bare library name, not a path or a target — and while the file computes set(libdir "${exec_prefix}/lib") it never uses that variable for anything. So the target contributes no -L, and naming it produces the same bare -larpack that falling through to an undefined target name does.

That also means the failure is not specific to consuming Cytnx. A plain project with no Cytnx involvement, using 3.8's own documented target as its README instructs:

find_package(arpack-ng CONFIG REQUIRED)
target_link_libraries(direct ARPACK::ARPACK)

links with -larpack and no -L as well. Every consumer of arpack-ng 3.8 from a non-system prefix is in that position; it is an upstream defect in that release's config, and exporting a different name on our side cannot repair it.

Two smaller points. The reverse direction is fine — a 3.8-built export naming ARPACK::ARPACK resolves against 3.9, because 3.9's alias provides that name too, giving the full path. And the include-directory difference between the two target shapes does not reach consumers either, since arpack is PRIVATE and reaches the export under $<LINK_ONLY:>, which carries the link entry only.

Where this genuinely does bite is a consumer with no arpack package config at all, and there find_dependency fails loudly at configure time rather than degrading — @IvanaGyro has ruled on that in the adjacent thread.


Generated by Claude Code

Comment thread CytnxBKNDCMakeLists.cmake
Comment thread cmake/CytnxConfig.cmake.in Outdated
# below, whose package config resolves BLAS/LAPACK itself when arpack is
# static.
set(BLA_VENDOR "@BLA_VENDOR@")
if(TARGET LAPACK::LAPACK AND NOT "@BLA_VENDOR@" STREQUAL "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject a pre-existing BLAS target too

This guard only notices an existing LAPACK::LAPACK, but CMake's FindLAPACK also appends BLAS::BLAS to the newly-created LAPACK::LAPACK when that BLAS target already exists, and FindBLAS will not replace an existing target. A consumer that called find_package(BLAS) first can therefore make an OpenBLAS or MKL-ILP64 Cytnx package link through that older LP64/non-MKL BLAS target even though no LAPACK::LAPACK existed yet; apply the same compatibility check/rejection to BLAS::BLAS before re-running the LAPACK/LAPACKE finder.

AGENTS.md reference: AGENTS.md:L118-L119

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 0028d4a.

The mechanism is exactly as described, and I verified it in the CMake modules before changing anything: FindBLAS.cmake:264 creates the target under if(BLAS_FOUND AND NOT TARGET BLAS::BLAS), and FindLAPACK.cmake:122-133 then removes BLAS_LIBRARIES from the LAPACK::LAPACK it is building and appends the existing BLAS::BLAS in their place. So a pre-existing BLAS target reaches Cytnx's link line through a LAPACK target that this config did create — which is precisely the case the old guard could not see.

Reproduced against a rendered CytnxConfig.cmake for a package built with BLA_VENDOR=OpenBLAS, consumed by a project that resolves reference BLAS first:

-- consumer's pre-existing BLAS::BLAS -> /usr/lib/x86_64-linux-gnu/libblas.so
-- resulting LAPACK::LAPACK -> BLAS::BLAS

Reference BLAS on the link line of an OpenBLAS-built Cytnx, and the old config emitted no warning at all. The new one warns, naming whichever of the two targets it found.

I kept it a warning rather than a rejection, for the same reason as the LAPACK::LAPACK case in this file: compatibility cannot be decided from inside the config. The pre-existing target may well be the matching implementation — a consumer that deliberately resolved OpenBLAS first is a legitimate and probably common ordering — and the only way to tell from here would be comparing against the build machine's library paths, which is the coupling this PR exists to remove.

Verified after the fix: the clashing consumer above now warns and still configures; a consumer with no pre-existing BLAS/LAPACK target stays silent (0 warnings). The two helper variables sit inside the existing block(SCOPE_FOR VARIABLES), checked not to leak into the consuming project.


Generated by Claude Code

@IvanaGyro
IvanaGyro force-pushed the claude/issue-1120-ng4p5n branch from 0deaca9 to ba97156 Compare August 4, 2026 19:53

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba97156712

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cmake/CytnxConfig.cmake.in Outdated
Comment on lines +114 to +116
find_package(arpackng CONFIG QUIET)
if(NOT arpackng_FOUND)
find_dependency(arpack-ng CONFIG REQUIRED)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse existing ARPACK targets before package lookup

When a downstream project has already created the ARPACK imported target (for example ARPACK::ARPACK/arpack from a subproject or toolchain) but does not install an arpackng/arpack-ng package config, this block still runs find_dependency(arpack-ng CONFIG REQUIRED) before loading CytnxTargets.cmake, so find_package(Cytnx) fails even though the exported link dependency is already satisfied. Guard the lookup on the existing ARPACK target, mirroring the cuTENSOR/cuQuantum target reuse below.

AGENTS.md reference: AGENTS.md:L118-L119

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 7bebba3.

Reproduced first, with a consumer that creates the target itself and has no arpack package config anywhere on CMAKE_PREFIX_PATH:

add_library(arpack INTERFACE IMPORTED)
add_library(ARPACK::ARPACK ALIAS arpack)
find_package(Cytnx CONFIG REQUIRED)

That failed at find_dependency(arpack-ng CONFIG REQUIRED)find_package(Cytnx) rejected over a dependency the project had already satisfied, exactly as described. The lookup is now guarded with if(NOT TARGET ARPACK::ARPACK), matching the cuTENSOR/cuQuantum reuse guard directly below it.

There is a second failure the guard picks up on the way: when such a project does also have an arpackng package config installed, the unguarded lookup reaches arpackngTargets.cmake, whose add_library(arpack SHARED IMPORTED) cannot redefine an arpack target the project already created. Skipping the lookup avoids that collision too.

I guarded on ARPACK::ARPACK alone rather than also on a bare arpack. ARPACK::ARPACK is the documented public name in both packagings, so it is the reliable signal; keying off arpack as well would let a project that defines only that name suppress a lookup the export may still need under its ARPACK::ARPACK spelling.

Verified after the fix: the consumer above configures; a consumer with neither the target nor a package config still fails loudly with the missing-package error, so the guard does not mask a genuinely absent dependency; and the normal path — package config present, no pre-existing target — is unchanged, including the vendor-clash warning behaviour.


Generated by Claude Code

Comment thread cmake/CytnxConfig.cmake.in Outdated
Comment on lines +114 to +116
find_package(arpackng CONFIG QUIET)
if(NOT arpackng_FOUND)
find_dependency(arpack-ng CONFIG REQUIRED)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse existing ARPACK targets before package lookup

When a downstream project has already created the ARPACK imported target (for example ARPACK::ARPACK/arpack from a subproject or toolchain) but does not install an arpackng/arpack-ng package config, this block still runs find_dependency(arpack-ng CONFIG REQUIRED) before loading CytnxTargets.cmake, so find_package(Cytnx) fails even though the exported link dependency is already satisfied. Guard the lookup on the existing ARPACK target, mirroring the cuTENSOR/cuQuantum target reuse below.

Useful? React with 👍 / 👎.

Comment thread CMakeLists.txt
# named here, and FindLAPACKE needs FindLAPACKEXT (which only delegates to
# CMake's own FindLAPACK). Add TMG and its closure here as well if cytnx ever
# starts requesting `LAPACKE COMPONENTS TMG`.
set(CYTNX_MORSE_FINDS LAPACKE LAPACKEXT)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Install BLASEXT with the LAPACKE finder

For installed OpenBLAS builds, CytnxConfig.cmake reruns find_dependency(LAPACKE MODULE REQUIRED) from the copied Morse modules, but the Morse FindLAPACKEXT.cmake used by FindLAPACKE invokes find_package(BLASEXT) before delegating to LAPACK. Since this install list only ships FindLAPACKE.cmake and FindLAPACKEXT.cmake, a clean downstream find_package(Cytnx) has no FindBLASEXT.cmake on CMAKE_MODULE_PATH and fails before it can recreate MORSE::LAPACKE; include BLASEXT in this closure as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not hold for the morse_cmake revision vendored here, so I have not changed the closure.

FindLAPACKEXT.cmake in cmake_modules/morse_cmake never calls find_package(BLASEXT). Its only find_package calls are for LAPACK:

46:  find_package(LAPACK QUIET REQUIRED)
48:  find_package(LAPACK QUIET)
92:    find_package(LAPACK QUIET)
102:    find_package(LAPACK QUIET)
112:    find_package(LAPACK QUIET)
131:find_package_handle_standard_args(LAPACKEXT DEFAULT_MSG

FindBLASEXT.cmake does exist in the submodule, but the only two finders that call it are FindFFTW.cmake and FindCBLAS.cmake, and cytnx requests neither.

The install list is also not the two files described. morse_install_finds() resolves the shared helper closure itself from the names given in CYTNX_MORSE_FINDS, so the installed lib/cmake/cytnx/morse/ holds nine files:

FindHeadersAndLibs.cmake   FindMorseCommon.cmake      LibrariesAbsolutePath.cmake
FindLAPACKE.cmake          FindMorseInit.cmake        MORSE-Copyright.txt
FindLAPACKEXT.cmake        ParseArguments.cmake       PrintFindStatus.cmake

The scenario is covered end to end by CI rather than by inspection alone: DownstreamFindPackage builds with the openblas-cpu preset (so USE_MKL=OFF, i.e. the find_dependency(LAPACKE MODULE REQUIRED) branch), installs, then configures tests/downstream_find_package against the install tree via CMAKE_PREFIX_PATH with nothing else on CMAKE_MODULE_PATH. A missing finder in that closure would fail there before MORSE::LAPACKE could be recreated. Both matrix legs are green on the reviewed commit.

Worth noting for a future change: FindLAPACKE does pull in one more finder, TMG, but only under if(LAPACKE_WITH_TMG), which cytnx does not set. CMakeLists.txt already carries a note beside CYTNX_MORSE_FINDS to add TMG and its closure if cytnx ever starts requesting LAPACKE COMPONENTS TMG.


Generated by Claude Code

@IvanaGyro
IvanaGyro force-pushed the claude/issue-1120-ng4p5n branch from ba97156 to 34555b6 Compare August 4, 2026 20:12

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 34555b65bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cmake/CytnxConfig.cmake.in Outdated
# consumer has no arpack package config installed, and would otherwise reach
# arpack-ng's own targets file, which cannot redefine an arpack target that
# project already created.
if(NOT TARGET ARPACK::ARPACK)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep ARPACK lookup until arpack exists

When Cytnx is built against arpackng 3.9, CMake serializes the ARPACK::ARPACK alias as $<LINK_ONLY:arpack> in CytnxTargets.cmake; a downstream that predefines only ARPACK::ARPACK from a toolchain or subproject but has no arpack package config now takes this guard, skips the lookup, and later links a bare -larpack because the exported arpack target was never created, so non-system prefixes still fail. Check for or recreate the exact target name the export uses before skipping the dependency lookup. Fresh evidence in this revision is that the new guard tests only ARPACK::ARPACK while the exported link can require arpack.

AGENTS.md reference: AGENTS.md:L118-L119

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, and fixed in 18ee3a9 — the guard now tests the exact name the export holds rather than ARPACK::ARPACK.

You are correct that my previous reply on the older thread did not cover this case. I argued there that keying only off ARPACK::ARPACK was the safe choice, but that leaves the 3.9-built export needing arpack while the guard passes on a name that may be a standalone target — exactly the gap you describe.

The build now records which name reached the export instead of either side guessing, since ARPACK::ARPACK being an alias is precisely what decides it:

get_target_property(cytnx_arpack_aliased ARPACK::ARPACK ALIASED_TARGET)
if(cytnx_arpack_aliased)
  set(CYTNX_ARPACK_TARGET "${cytnx_arpack_aliased}")
else()
  set(CYTNX_ARPACK_TARGET ARPACK::ARPACK)
endif()

and CytnxConfig.cmake.in guards on if(NOT TARGET @CYTNX_ARPACK_TARGET@). Against conda-forge's arpackng 3.9 the build reports ARPACK target in the export: arpack, CytnxTargets.cmake carries $<LINK_ONLY:arpack>, and the generated config reads if(NOT TARGET arpack) — the three now agree by construction rather than by assumption.

Verified against the real generated config, with no arpack package config on CMAKE_PREFIX_PATH:

consumer predefines behaviour
arpack + ARPACK::ARPACK alias guard holds, configures — the case the guard is for
only ARPACK::ARPACK guard does not hold, lookup runs and fails loudly on the missing package

So the silent -larpack degradation you predicted can no longer happen: the only consumer that skips the lookup is one that already supplies the target the export names. Both presets reconfigure clean and report the same exported name.


Generated by Claude Code

Comment thread cmake/CytnxConfig.cmake.in Outdated
# LAPACK::LAPACK alone does not resolve the LAPACKE_* entry points (nor
# provide <lapacke.h>). One FindLAPACKE run recreates both targets cytnx
# links: it pulls in FindLAPACKEXT, which calls CMake's FindLAPACK.
find_dependency(LAPACKE MODULE REQUIRED)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Force the Cytnx LAPACKE finder to run

When another dependency package in the same directory has already used find_dependency(LAPACKE MODULE REQUIRED) with its own module, CMake 3.26+'s identical-call optimization can skip this call even though this block just prepended Cytnx's Morse module path; then MORSE::LAPACKE is never created and including CytnxTargets.cmake leaves the exported Cytnx::cytnx target with an unresolved dependency. Use find_package(LAPACKE MODULE REQUIRED) here, or explicitly verify MORSE::LAPACKE and rerun/diagnose before loading the export.

AGENTS.md reference: AGENTS.md:L118-L119

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 43d6dd7.

I checked the mechanism in CMakeFindDependencyMacro.cmake before acting, because I expected the older if(NOT ${dep}_FOUND) short-circuit. It is the one you describe: 3.15 removed the optimization, 3.26 restored it keyed on a hash of the call —

string(SHA256 cmake_fd_call_hash "${dep};${ARGN};${${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED}")
if(_CMAKE_${dep}_${cmake_fd_call_hash}_FOUND)

and its own documentation names this exact hazard: "the heuristic cannot account for ambient variables that affect package behavior… package configuration files should avoid setting such variables before their calls to find_dependency." This block sets two of them — the CMAKE_MODULE_PATH prepend and BLA_VENDOR.

Reproduced with a package whose config runs find_dependency(LAPACKE MODULE REQUIRED) against its own FindLAPACKE, found ahead of ours in the same directory:

another package called it first = OFF -> MORSE::LAPACKE created
another package called it first = ON  -> MORSE::LAPACKE MISSING -- our lookup was optimized out

The branch is now find_package(LAPACKE MODULE). find_package has no such memo, and MODULE closes the config-mode hole covered in the adjacent thread. I dropped REQUIRED and verify the target after endblock() instead, so a genuine miss is still reported the way find_dependency would have — as a package-not-found reason rather than a bare fatal error:

-- Cytnx_FOUND=0
-- reason: Cytnx requires LAPACKE, but MORSE::LAPACKE was not created by the FindLAPACKE shipped with Cytnx. …

The check sits outside the block deliberately: block(SCOPE_FOR VARIABLES) would swallow Cytnx_FOUND / Cytnx_NOT_FOUND_MESSAGE before find_package could read them, while the imported target it tests is directory-scoped and still visible.

Verified after the fix: the diamond case above now creates the target; the plain consumer is unchanged; the unfindable-LAPACKE case reports the message above instead of failing at generate time; and both the openblas-cpu and mkl-cpu presets configure clean (the MKL branch renders the check inert, if(NOT ON AND …)).


Generated by Claude Code

Comment thread CytnxBKNDCMakeLists.cmake Outdated
# FindLAPACKEXT, which calls CMake's own FindLAPACK under the BLA_VENDOR set
# above, which in turn finds BLAS. MORSE::LAPACKE and LAPACK::LAPACK both
# exist afterwards.
find_package( LAPACKE REQUIRED)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Force module mode for the build LAPACKE lookup

If a build environment sets CMAKE_FIND_PACKAGE_PREFER_CONFIG=TRUE and has a LAPACKEConfig.cmake that exports the usual variables but not Morse's MORSE::LAPACKE target, CMake's basic find_package signature searches Config mode first (per cmake --help-command find_package) and this line can bypass the bundled Morse finder; the new target_link_libraries(... MORSE::LAPACKE ...) then fails at generate time even though the old variable-based link path would have configured. Use find_package(LAPACKE MODULE REQUIRED) here, matching the installed config's module-mode lookup.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 43d6dd7 — the line is now find_package(LAPACKE MODULE REQUIRED).

Reproduced the bypass before changing anything, with a LAPACKEConfig.cmake that sets LAPACKE_LIBRARIES/LAPACKE_FOUND and nothing else, Morse on CMAKE_MODULE_PATH, and CMAKE_FIND_PACKAGE_PREFER_CONFIG=TRUE:

find_package(LAPACKE <basic> REQUIRED) -> MORSE::LAPACKE MISSING (config mode won)
find_package(LAPACKE MODULE  REQUIRED) -> MORSE::LAPACKE created

So the basic signature really does reach a config package that cannot satisfy target_link_libraries(cytnx PUBLIC MORSE::LAPACKE ...), and the failure would land at generate time rather than at the lookup.

The same one-word fix went into the generated config's LAPACKE branch, which had the same exposure — see the adjacent thread, where it is paired with a find_package/find_dependency change for a second reason. Build and installed config now agree on module mode.

Verified after the fix by configuring both the openblas-cpu and mkl-cpu presets on the rewritten branch; both configure clean.


Generated by Claude Code

Cytnx::cytnx propagated several dependencies as absolute paths detected on the
build machine: ${LAPACKE_INCLUDE_DIRS} and ${LAPACKE_LIBRARIES} in the OpenBLAS
branch of CytnxBKNDCMakeLists.cmake, ${LAPACK_LIBRARIES} in the MKL branch, and
${CUTENSOR_INCLUDE_DIRS} / ${CUQUANTUM_INCLUDE_DIRS} with their libraries under
USE_CUTENSOR / USE_CUQUANTUM. install(EXPORT) copies those verbatim into
CytnxTargets.cmake, so the installed package only resolved on a machine whose
dependencies sit at the same paths.

They cannot simply be hidden behind $<BUILD_INTERFACE:> either, because they are
genuine public usage requirements: the installed header
backend/lapack_wrapper.hpp includes <lapacke.h>, and cytnx_error.hpp /
Network.hpp include <cutensor.h> / <cutensornet.h> under the PUBLIC
UNI_CUTENSOR / UNI_CUQUANTUM definitions.

Name each dependency by imported target and let the consumer rediscover it:

  * MORSE::LAPACKE (created by the vendored Morse FindLAPACKE) together with
    LAPACK::LAPACK on the OpenBLAS path. Both are required: where LAPACKE is a
    standalone library rather than one embedded in LAPACK, MORSE::LAPACKE
    carries liblapacke alone, and cytnx also calls plain BLAS/LAPACK entry
    points that the removed ${LAPACK_LIBRARIES} entry used to supply.
  * LAPACK::LAPACK on the MKL path.
  * CUTENSOR::CUTENSOR and CUQUANTUM::CUQUANTUM, now created by
    cmake/Modules/FindCUTENSOR.cmake and cmake/Modules/FindCUQUANTUM.cmake.

CytnxConfig.cmake reruns those finders against the consumer's own environment
before including CytnxTargets.cmake. They share one block(SCOPE_FOR VARIABLES),
which holds the single CMAKE_MODULE_PATH prepend and the BLA_VENDOR restore that
all of them need, so neither leaks into the consuming project; the imported
targets the finders create are directory-scoped and outlive the block. Boost,
CUDAToolkit and OpenMP stay outside it, because a consumer legitimately reads the
result variables those set and the block would swallow them. USE_MKL selects the
same branch there that it selects in the build. The Morse finders are loaded by
module path rather than by including Morse's top-level MorseInit.cmake, which
would rewrite project-wide options in the consumer.

The finder modules ship with the package: morse_install_finds() for the install
tree, and a matching file(COPY) of the same closure for the build tree, which
export(PACKAGE Cytnx) makes a consumable package too. Serializing the detected
targets with morse_export_imported_target() would have written the build
machine's paths straight back in.

ARPACK stays a plain absolute path here: it is located with a bare
find_library() and has no finder module for a consumer to rerun.

Co-Authored-By: Claude <noreply@anthropic.com>
IvanaGyro and others added 6 commits August 4, 2026 20:38
CytnxConfig.cmake sets BLA_VENDOR to the value cytnx was built with before
the LAPACK/LAPACKE lookup, and claimed that this keeps a consumer from binding
to a different implementation or integer ABI. It does not. Neither finder
replaces a target that already exists: FindLAPACK creates the imported target
under `if(LAPACK_FOUND AND NOT TARGET LAPACK::LAPACK)`, and FindBLAS guards
BLAS::BLAS the same way. When another dependency in the consuming project has
already run find_package(LAPACK), the existing target survives and BLA_VENDOR
steers nothing. An MKL ILP64 build of cytnx then links against an LP64 or
reference target and miscomputes instead of failing loudly.

A pre-existing BLAS::BLAS is enough on its own, even when no LAPACK target
exists: FindLAPACK removes BLAS_LIBRARIES from the LAPACK::LAPACK it creates
and links that BLAS target in their place, so the consumer's BLAS ends up on
cytnx's link line through a LAPACK target that this config did create. A
consumer that resolves reference BLAS first and then finds a Cytnx built
against OpenBLAS gets LAPACK::LAPACK -> BLAS::BLAS -> libblas.so.

BLA_VENDOR can only steer a lookup that has not happened yet, so say so in the
comment and make the remaining case visible: warn when either target already
exists, naming the ones found, the vendor cytnx expects, and the two ways out
(resolve BLAS and LAPACK with that BLA_VENDOR before find_package(Cytnx), or
find Cytnx first).

A warning rather than a hard error, because compatibility cannot be decided
from here -- the pre-existing target may well be the same implementation, and
rejecting it would break that legitimate ordering.

Co-Authored-By: Claude <noreply@anthropic.com>
FindCUTENSOR.cmake and FindCUQUANTUM.cmake raise a fatal error when their
*_ROOT is unset, before reaching the guard that skips creating an existing
imported target. CytnxConfig.cmake ran them unconditionally, so a consuming
project that already provides CUTENSOR::CUTENSOR or CUQUANTUM::CUQUANTUM failed
find_package(Cytnx) over a dependency it had in fact already satisfied.

Skip each lookup when its target exists, which is all the exported cytnx target
needs.

Co-Authored-By: Claude <noreply@anthropic.com>
Supporting build-tree consumers meant copying Morse's FindLAPACKE closure, and
cytnx's own SDK finders, into the build directory so that one generated
CytnxConfig.cmake could reach them under a single relative path. That left a
second copy of every finder to keep in step with the installed one, and the
Morse staging had to reconstruct the file list from morse_find_core, a variable
morse_install_finds() only leaves behind because it happens to be a macro.

The two trees do not need the same path, only the same config logic. Configure
the one template twice instead, injecting the module search paths: the
build-tree config reads the finders straight out of the source tree, and the
installed config reads the copies inside the package, which stay the sole
responsibility of morse_install_finds(). Nothing is staged into the build
directory and no file list is restated.

FindMorseInit.cmake includes ParseArguments by bare name and that module sits a
directory above find/, so the build-tree search path covers both directories;
the installed bundle is flat and needs one.

Co-Authored-By: Claude <noreply@anthropic.com>
Both provider branches in CytnxBKNDCMakeLists.cmake called finders whose work
another finder in the same branch already does:

  * The MKL branch ran find_package(BLAS) before find_package(LAPACK), but
    CMake's FindLAPACK looks for BLAS itself.
  * The OpenBLAS branch ran find_package(BLAS) and find_package(LAPACK) before
    find_package(LAPACKE), but morse_cmake's FindLAPACKE includes FindLAPACKEXT,
    which calls CMake's FindLAPACK -- which in turn finds BLAS. Both
    MORSE::LAPACKE and LAPACK::LAPACK, the two targets this branch links, exist
    after that one call.

Drop the redundant calls, so each branch states one lookup and BLA_VENDOR is
consulted by exactly the finder that acts on it. CytnxConfig.cmake.in mirrors
the same split for the consumer's re-find: the MKL build runs
find_dependency(LAPACK), and the OpenBLAS build runs find_dependency(LAPACKE
MODULE) alone rather than a LAPACK lookup followed by a LAPACKE one.

Define UNI_OPENBLAS on the cytnx target, matching the UNI_MKL the MKL branch
already applies. CYTNX_VARIANT_INFO has always advertised the variant; this
makes it visible to code compiling against the installed headers as well.

Testing: openblas-cpu and mkl-cpu debug presets build and pass ctest; the
exported target still names MORSE::LAPACKE and LAPACK::LAPACK for the OpenBLAS
build and LAPACK::LAPACK for the MKL build.

Co-Authored-By: Claude <noreply@anthropic.com>
CytnxBKNDCMakeLists.cmake resolved arpack with find_library(ARPACK_LIB) and the
main CMakeLists.txt linked ${ARPACK_LIB} into the cytnx target. libcytnx is a
STATIC archive, so even that PRIVATE dependency reaches the export -- as
$<LINK_ONLY:/abs/path/to/libarpack.so> -- which pins the installed package to
the build machine's library layout, the same defect this branch already fixed
for LAPACKE, BLAS/LAPACK, cuTENSOR and cuQuantum.

Use arpack-ng's own CMake package instead: find_package(arpackng CONFIG) defines
ARPACK::ARPACK, cytnx links that target, and CytnxConfig.cmake re-finds the
package so a consumer binds its own arpack. Both packagings cytnx builds against
ship a CMake package config -- conda-forge's arpack for manylinux and macOS,
Alpine's arpack-dev for musllinux -- so no finder module of cytnx's own is needed
and none is installed.

ci-gpu_tests.yml is the one job that did not already provision arpack that way:
the self-hosted GPU runner supplies its dependencies from apt, and apt's arpack
installs the library and headers with no package config, so the configure step
failed outright. It now creates a workspace-local prefix holding conda-forge's
arpack and points CMAKE_PREFIX_PATH at it. micromamba is fetched as a single
static binary into the runner's temp dir rather than installing a conda
distribution, so nothing outside the workspace changes and the job keeps the
plain non-login shell its other steps use.

The lookup accepts either spelling of the package name: arpack-ng renamed it
from arpack-ng to arpackng in 3.9, and Alpine -- which the musllinux wheels
build against -- ships 3.8.0. The two also differ in how they define
ARPACK::ARPACK. 3.9 makes it an alias of a target named arpack, and CMake
records the aliased target's name when it writes an export, so CytnxTargets.cmake
names arpack; 3.8 makes it an imported target in its own right, so the export
names ARPACK::ARPACK. Either way the export holds a target name rather than a
path, and the same two-step lookup in the generated config recreates whichever
name it holds.

The generated config skips that lookup when the consuming project already
supplies ARPACK::ARPACK, mirroring the cuTENSOR/cuQuantum reuse guard beside it.
A project that builds arpack-ng as a subproject, or takes the target from a
toolchain, satisfies the exported dependency without having an arpack package
config installed; running the lookup anyway failed find_package(Cytnx) over a
dependency that was in fact present, and would otherwise reach arpack-ng's own
targets file, which cannot redefine an arpack target that project already
created. A consumer with neither the target nor a package config still fails
loudly.

The lookup runs after the BLAS/LAPACK provider rather than before it: a
statically built arpack's config calls find_dependency(BLAS) and
find_dependency(LAPACK) unless those targets already exist, so resolving cytnx's
provider first stops arpack from binding the build to a vendor other than the one
BLA_VENDOR names.

Testing, against conda-forge's arpack 3.9.1 nompi build -- the one the wheel
provisioning scripts and CI install: CytnxTargets.cmake carries
$<LINK_ONLY:arpack> and no libarpack path in both the build tree and the install
tree, and tests/downstream_find_package's consumer configures, links and runs
against each. Against Alpine's arpack-dev 3.8.0-r3, unpacked from the real apk:
the fallback name resolves, the export names ARPACK::ARPACK, and a consumer
configures against it. A consumer that defines ARPACK::ARPACK itself with no
package config on CMAKE_PREFIX_PATH configures where it previously failed, while
one with neither still reports the missing package.

Co-Authored-By: Claude <noreply@anthropic.com>
The DownstreamFindPackage job builds a consumer against the install tree, which
proves the package resolves -- but only on the runner that produced it. An
absolute library path baked into INTERFACE_LINK_LIBRARIES resolves there too,
so the consumer step passes while the package is unusable anywhere else. That
is the exact defect this workflow is meant to guard, and it went unnoticed for
arpack.

Read the property out of the installed CytnxTargets.cmake after the install
step and fail on any entry that is a filesystem path, plus require arpack to
still be named: arpack is cytnx's one PRIVATE dependency, exported only because
a static archive leaves its symbols for the consumer's final link, so silently
losing it from the export would break that link with nothing else in this job
to catch it. The name match is case-insensitive because which one the export
holds depends on the arpack-ng packaging -- "arpack" where ARPACK::ARPACK is an
alias, "ARPACK::ARPACK" where it is an imported target in its own right.

Verified against install trees built before and after arpack moved to an
imported target: the check reports
"/usr/lib/x86_64-linux-gnu/libarpack.so" and exits 1 on the former, passes on
the latter.

Co-Authored-By: Claude <noreply@anthropic.com>
@IvanaGyro
IvanaGyro force-pushed the claude/issue-1120-ng4p5n branch from 34555b6 to 1a93594 Compare August 4, 2026 20:39
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix exported CMake usage requirements and relocatable dependencies

1 participant