Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add nix support #918

Merged
merged 61 commits into from
Oct 20, 2023
Merged

Conversation

jake-arkinstall-quantinuum
Copy link
Contributor

Not yet ready for review. Aside from possible design changes, documentation and test automation need to be added too. Testing on MacOS is also required. Windows support is not generally available in Nix at this stage (to my knowledge).

Nix is an alternative, hermetic way of managing packages. With this PR, I've added support for building within Nix.

The nix.flake file specifies the core nix dependencies. It also specifies where the build instructions can be found - these are bundled into a new nix-support directory.

The structure of nix-support avoids having nix files scattered throughout the project. The package files are:

  • symengine.nix: provides a derivation of SymEngine built to suit the needs of this project. Specifically, through symengine.patch, dependencies are linked as required. Additionally, symengine.nix provides both static and shared library versions of SymEngine.
  • third-party-python-packages.nix - provides packages missing from nixpkgs. These are:
    • pybind11_json
    • qwasm
    • lark-parser
    • types-pkg_resources
  • libs.nix - provides the following as static and shared derivations:
    • tklog
    • tkrng
    • tkassert
    • tktokenswap
    • tkswm
  • tket.nix - provides tket, as both static and shared derivations
  • pytket.nix - provides the binders and pytket, using only the shared derivations.

An additional change is to setup.py, which now has a nix-specific build extension. This uses the environment variables provided by nix to find tklog, tket, and binders. CMakeLists.txt for tket is now also told not to error on -wmaybe-uninitialized, because of a false positive that crops up in GCC 12.2. If this isn't wanted for general builds, this flag can be added as a patch for nix-specific builds.

These derivations are all provided through fixed point combinators, so that flake.nix can use them as overloads to pkgs.

Additional changes that are required for compilation, though not specific enough for this PR, are:

  • use of size_t in the codebase. The relevant <c....> headers only guarantee that std::size_t is provided, not C's size_t. Most implementations accept this, but GCC 12.2 does not.
  • Missing break statements in tket/include/tket/ArchAwareSynth/SteinerTree.hpp. Although an assert is fired on default paths, indirectly blocking fallthrough, the compiler can't always detect this, and warns on possible fallthrough. This causes a failed build due to -Werror.

Example usage (replace the github organisation once merged, and this requires the above additional changes to run) is below. Note that the machine does not need any dependencies, including python or GCC, installed in advance. It takes some time to compile - and gives my threadripper a run for its money, but promises a reliable build.

$ nix develop --refresh github:jake-arkinstall-quantinuum/tket?ref=nix-restructure

$ python3
Python 3.10.11 (main, Apr  4 2023, 22:10:32) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pytket
>>> foo = pytket.Circuit(2)
>>> foo.X(0)
[X q[0]; ]
>>> foo.H(1)
[X q[0]; H q[1]; ]
>>>

jake-arkinstall-quantinuum and others added 30 commits July 3, 2023 14:08
…n GCC 12.2

tket/src/Circuit/CircUtils.cpp lines 1230 and 1237 clearly only deference the std::optional
within a checked branch. However, GCC 12.2 gives:

```
/build/tket/src/Circuit/CircUtils.cpp: In function 'std::tuple<tket::Circuit, std::array<SymEngine::Expression, 3>, tket::Circuit> tket::normalise_TK2_angles(Expr, Expr, Expr)':
/build/tket/src/Circuit/CircUtils.cpp:1237:13: error: '*(double*)((char*)&b_eval + offsetof(std::optional<double>,std::optional<double>::<unnamed>.std::_Optional_base<double, true, true>::<unnamed>))' may be used uninitialized [8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wmaybe-uninitialized-Werror=maybe-uninitialized8;;]
 1237 |     *b_eval -= 3;
      |     ~~~~~~~~^~~~
/build/tket/src/Circuit/CircUtils.cpp:1148:25: note: '*(double*)((char*)&b_eval + offsetof(std::optional<double>,std::optional<double>::<unnamed>.std::_Optional_base<double, true, true>::<unnamed>))' was declared here
 1148 |   std::optional<double> b_eval = eval_expr_mod(b, 4);
      |                         ^~~~~~
/build/tket/src/Circuit/CircUtils.cpp:1230:13: error: '*(double*)((char*)&a_eval + offsetof(std::optional<double>,std::optional<double>::<unnamed>.std::_Optional_base<double, true, true>::<unnamed>))' may be used uninitialized [8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wmaybe-uninitialized-Werror=maybe-uninitialized8;;]
 1230 |     *a_eval -= 3;
      |     ~~~~~~~~^~~~
/build/tket/src/Circuit/CircUtils.cpp:1147:25: note: '*(double*)((char*)&a_eval + offsetof(std::optional<double>,std::optional<double>::<unnamed>.std::_Optional_base<double, true, true>::<unnamed>))' was declared here
 1147 |   std::optional<double> a_eval = eval_expr_mod(a, 4);
```

An alternative fix is to apply a `#pragma GCC diagnostic ignore "-Wmaybe-uninitialized"` to the regions in question.
…not fall through.

For the modified cases, the switch statements would fall through if no return path was hit.
Although the assert is fired when no return is performed anyway, this isn't clear to the
compiler, and a fallthrough warning (thus failure due to -Werror) occurs.

Adding these breaks makes the intent clear to the compiler and avoids such issues.
This isn't conceptually ideal, but is harmless for this case.
Global size_t is referred to in these files and I didn't want
to make too many changes here. As per the standard, size_t
imported from the <c...> headers only guarantee its presence
in the std namespace, not the global namespace, though
implementations may take liberties.
Some key points to note about this implementation. First,
the imported source files avoid default.nix itself such that
changes to default.nix will not be considered changes to the
project source, and thus won't trigger unnecessary rebuilds.

Second, Nix+CMake awkwardly double-prepended the nix store
path to the include directory, rendering it e.g.:
/nix/store/[hash]-tklog//nix/store/[hash]-tklog
To avoid this, a post phase is added to strip this unwanted
behaviour, allowing it to be imported from other projects
without failure.
Refer to d3ae2c96 (Added default.nix for tklog., 2023-07-03) for
implementation details.
See d3ae2c96 (Added default.nix for tklog., 2023-07-03) for
implementation details.
See d3ae2c96 (Added default.nix for tklog., 2023-07-03) for
implementation details.
See d3ae2c96 (Added default.nix for tklog., 2023-07-03) for
implementation details.
This exports an attrset of name->package for each package
within libs.
pkgs.symengine doesn't propagate its dependencies, so those
are injected explicitly via pkgs.symengine.buildInputs.

The same implementation details as the implementation in
d3ae2c96 (Added default.nix for tklog., 2023-07-03) apply.
The concept is somewhat different to the original intent of setup.py,
simply due to keeping Nix in control of the build. Nix, rather than
setup.py, compiles binders/. It then passes that - along with python
package dependencies such as numpy etc - to the setup process. It
also passes a flag: USE_NIX=1

The setup process now detects USE_NIX and chooses a NixBuild build_ext
to put the pieces together, rather than the previous options of
CMake or Conan. In the absense of USE_NIX, the setup process is unchanged.

Whether or not "doing nothing" is the appropriate form of NixBuild,
I do not yet know. Probably not. But until we get to that stage,
we need to solve another issue: `types-pkg_resources` is a listed
dependency, and we aren't providing it. It isn't something that's in
nixpkgs, and I'm struggling to find any useful information about
it online.
Added nix support for types-pkg_resources via fetchPypi,
and established build of pytket. At current, tests cause
permission errors so are disabled. This must be remedied
in future, or otherwise provided in a separate derivation
such that tests can be run.

At current, no confirmation that the build is successful
and that libraries are available to pytket when running.
In particular, at the moment I'm making .a files, rather
than .so files, available for tket and libs, which may
or may not work. If it doesn't work, an alternative build
will need to be added.
…red libraries on request, and provided the shared alternatives to pytket.
- Overloaded symengine (static and shared), such that build inputs
  are propagated
- Patch libtket.so so that symengine is listed as a needed dependency
- Symlink rather than copy libtket.so to pytket's _tket dir so that
  fixupPhase doesn't remove symengine
- Manually setting pytket.__version__ to 0.0.0, as scm-based versioning
  is somewhat messy with Nix
- Other small structural changes

This needs a lot of cleanup before any PR would be possible, but as
pytket can be imported for the first time with this Nix approach, it's
worthy of a commit.
…xed point combinators instead of exposing derivations directly
…, and adding a workflow to run nix flake check
@cqc-alec
Copy link
Collaborator

Looks good, I think this just needs:

  • editing the workflow script to run on both ubuntu-22.04 and macos-12 (using the matrix method, see other workflow scripts for examples);
  • adding a paragraph to the top-level README.md, probably just before the "API documentation" section.

@jake-arkinstall-quantinuum
Copy link
Contributor Author

I've now added python tests, so nix flake check is (I think) complete.

Looks good, I think this just needs:

  • editing the workflow script to run on both ubuntu-22.04 and macos-12 (using the matrix method, see other workflow scripts for examples);

I'll look into that now.

  • adding a paragraph to the top-level README.md, probably just before the "API documentation" section.

I've now added this. It points to a README.md in the nix-support directory. I've also added an example usage flake within there, and documented that (lightly) to provide an example of what simple usage might look like.

@jake-arkinstall-quantinuum
Copy link
Contributor Author

Test matrix now added. This will be the first time this approach has been tried on MacOS. I'll run it on my macbook first and diagnose any issues that may arise, then once that's done I'm happy to promote this from a draft PR.

…o symengine, not to the mythical LIB library.
…n -DINSTALL_NAME_DIR=OFF is passed (which the Nix build now does). Added -flat_namespace link arg to pytket binders to avoid symbol confusion when using pybind11 (specifically, typecast.hpp's dynamic casts failed, though the types were 'correct' and reinterpret_casts worked). Cleaned up the nix section of setup.py as libtket is automagically included. Updated flake.nix to take advantage of an upstream change to symengine which makes shared library usage easier to deal with.
@jake-arkinstall-quantinuum
Copy link
Contributor Author

All tests passing on MacOS now. It was surprisingly awkward due to dynamic_casts failing, and it seems to be thanks to dylib's two-level namespaces (identified thanks to https://developer.apple.com/forums/thread/693914).

I want to verify my changes on Linux again, do some tidying around the edges, then I'll promote this to a full PR.

@jake-arkinstall-quantinuum jake-arkinstall-quantinuum marked this pull request as ready for review October 19, 2023 16:57
@cqc-alec cqc-alec self-requested a review October 20, 2023 08:23
src = tket-without-tests;
nativeBuildInputs = [ super.cmake ];
propagatedBuildInputs = super.tklibs
++ [ super.boost super.symengine super.eigen super.nlohmann_json ];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we specify the versions of these libraries anywhere or does nix just pick up the latest available?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The versions of the packages are defined by the nixpkgs commit that the flake.lock file is pinned to.

The typical way of bumping the packages when they're available in a newer version of nixpkgs is to run "nix flake update", which updates the lock file.

If a version is needed that isn't in nixpkgs yet, or if the version in nixpkgs is too new, or if something needs to be tweaked, the derivation of the dependency can be defined in a nix file within tket (this is what I did with jsonschema and SymEngine, for example).

src = super.fetchFromGitHub {
owner = "CQCL";
repo = "qwasm";
rev = "35ebe1e2551449d97b9948a600f8d2e4d7474df6";
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's likely to be a certain amount of bitrot here as dependency versions get updated; maybe we could prevent this with some ugly CI checks but probably not worth it for now.

Copy link
Collaborator

@cqc-alec cqc-alec left a comment

Choose a reason for hiding this comment

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

LGTM!

@cqc-alec cqc-alec merged commit 7220b48 into CQCL:develop Oct 20, 2023
53 checks passed
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.

3 participants