Skip to content

Build wheels and distribute to PyPI (pip) from GitHub Actions#1465

Merged
jakelishman merged 13 commits into
qutip:masterfrom
jakelishman:packaging-master
Mar 27, 2021
Merged

Build wheels and distribute to PyPI (pip) from GitHub Actions#1465
jakelishman merged 13 commits into
qutip:masterfrom
jakelishman:packaging-master

Conversation

@jakelishman

@jakelishman jakelishman commented Mar 17, 2021

Copy link
Copy Markdown
Member

This patch obsoletes #1429, by applying the packaging changes to the current version of master rather than needing the new structure on dev.major. This patch should (mostly) apply cleanly to dev.major as well, so merging this up will be easy - the new automated systems in setup.py also mean that the distribution can handle both the current master and the dev.major package layouts with no changes.

The rest of this text is very similar to the original message in #1429.


This patch overhauls how QuTiP is packaged and distributed. Once this patch is merged, there will be an option on QuTiP's GitHub Actions tab for people who have write permission on qutip/qutip to build all wheels and optionally release the build to PyPI, making it available on pip as a binary release.

The major changes are in the files pyproject.toml, setup.py, setup.cfg and .github/workflows/build.yml, while MANIFEST.in and a new file VERSION were also touched.

Distribution changes:

  • there is a GitHub Action which will build all the wheels for CPython 3.6+ on Linux (manylinux1 x86 and x86_64), macOS and Windows (32- and 64-bit). This action is triggered manually, and the wheels will be available for download afterwards. Optionally, the action will push to PyPI, making the version immediately available on pip by binary release.
  • .cpp files are now distributed with the wheels but not added to source control. This is a step towards a full Cython-free delivery of the QuTiP source (binary releases never require Cython), but right now I think our setuptools machinery doesn't quite handle that.

Changes to setup.py

  • all package metadata is moved out into setup.cfg, which is easier to read and change, and modern setuptools' preferred way of doing things
  • all directory-tree access is done in a completely platform-agnostic manner, rather than just hoping that Unix-style paths will work on Windows
  • the file is tidied and structured into a small script, with separated components
  • build requirements are moved into pyproject.toml including listing setuptools as the build method, in accordance with PEP 517. Any PEP 517-complaint installation and build process will now enforce the presence of numpy, scipy and cython (and setuptools) before attempting the build, so no more need for attempted import guards.
  • packages are now discovered rather than explicitly stated, using standard setuptools machinery
  • Cython extension modules are now discovered rather than explicitly stated, using globbing
  • version information is now stored in a single-line file VERSION. This is mostly for CI reasons; it's much easier to parse and override at the CI level when it's a single file, rather than attempting sed s/.../.../ setup.py or something crazy like that. The validity of the version string is tested by regex.
  • local version information is now always appended, even if git isn't present - the local version is just then "nogit" rather than the git shorthash
  • setup.py gained a new --release flag or environment variable CI_QUTIP_RELEASE; the only effect right now is to suppress the local identifier on the version information, replacing the old ISRELEASED boolean flag in the code

Notes on the wheels

Currently I build only for CPython. I haven't tried to build pypy wheels, but we may support it - Cython does in theory, but I think complex numbers and raw pointers aren't handled completely smoothly, and we make heavy use of both. We get manylinux1 builds for Linux, which is the oldest manylinux spec. This can be updated in the future, but I'm not sure I see a need; all our heavy mathematical lifting is either done by custom code (which is mostly immune) or BLAS/LAPACK (which we link to dynamically), so I don't think there's much to be gained. I don't know if the macOS images will run on the new M1 chips, but if not, there's not much that can be done there until GitHub Actions adds the necessary cross-compilation headers and libraries to their CI.

Examples

You can see the result of this upload on the testing PyPI server: https://test.pypi.org/project/qutip-jakelishman (version 4.6.0a1 is the current state of master). I ran the CI action from my fork, with a temporary commit that changed the server to the testing archive (and the package name). Here's a screenshot of what the deployment screen will look like:

GitHub Actions screen when triggering the build and deploy action

@coveralls

coveralls commented Mar 17, 2021

Copy link
Copy Markdown

Coverage Status

Coverage remained the same at 63.42% when pulling 3d062cd on jakelishman:packaging-master into f7bc01b on qutip:master.

Comment thread .github/workflows/build.yml Outdated
workflow_dispatch:
inputs:
confirm_ref:
description: "Enter branch/tag name to deploy to PyPI, or leave blank to only build wheels."

@hodgestar hodgestar Mar 18, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To me this reads as "I can type the name of the branch I want to deploy to PyPI here", but the intention seems to be that one should type the name of this branch to confirm. Maybe we can change the start to `Enter the name of this tag or branch to confirm deploying to PyPI ..."?

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.

Yeah, this could be better. I struggled with this wording, especially because there's also a minor space constraint - if you look in the picture in the PR text above, you can see that this text gets wrapped into really short lines, so I didn't want to write something that would end up 5 lines long and have no-one read it.

Perhaps?

Confirm branch name to deploy to PyPI, leave blank to only build wheels.

This is a wordsmithing problem, so I bet @ajgpitch will enjoy it too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Confirm branch name to deploy to PyPI, leave blank to only build wheels. looks good to me. Or perhaps Enter branch name to confirm deploying to PyPI, leave blank to only build wheels?

Maybe anything other than blank or the correct branch name should fail the build? That is very much a nice to have though.

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.

Yeah, it's probably better to hard fail if something's typed but it's the wrong thing. Unfortunately, there's no way to do pre-processing on these values, so I'll have to change the "canary" job that just exists to provide a visible tick saying "will attempt to deploy" to explicitly kill the whole run if the string is non-empty and unequal to the branch name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can a check on github.repository or github.repository_owner work as well? See related PR for Mitiq unitaryfoundation/mitiq#597

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 trigger is a manual click action - you have to press a button on GitHub so there's no danger of "accidentally" running it on a fork.

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.

Or were you suggesting we do it based on a label? In this, the only way to get a build is to type in a particular branch name, which implies that you have to have produced a special branch/git tag for the release, so that might be quite similar to what you were asking?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I quite like having to do a manual release rather than using a tag because it allows one to rerun the build if a problem occurs and to try things out without filling the github repository with tags named things like "test-457".

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 is the reason I went for this method in the end. I think when I originally proposed automatic deployment I talked about it being tag-based, but I think this is cleaner.

You can also force-push a tag that had a failed release attached to it, though - that's not doing any harm, because a tag isn't truly public until the release is made successfully.

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.

Can a check on github.repository or github.repository_owner work as well? See related PR for Mitiq unitaryfund/mitiq#597

So this file uses a slightly different mechanism - in mitiq you're tying the release action to GitHub's "make release" button, whereas here I tie it to you manually running this action. One major advantage to this method is that admins can build a full set of wheels without making a public release, which is a bigger deal for us because our wheels involve very nontrivial compilation, so there's rather more chance of something going wrong.

Comment thread .github/workflows/build.yml Outdated
Comment thread .gitignore
Comment thread qutip/cy/brtools.pyx

@hodgestar hodgestar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I left some small suggestions and questions, but otherwise this looks great! Thank you @jakelishman.

@hodgestar

Copy link
Copy Markdown
Contributor

The CI failure on macOS, Python 3.8: XCode 12 is because the tests took too long. Not sure if this is just a Travis issue or a pre-existing problem or something new.

@jakelishman

Copy link
Copy Markdown
Member Author

The macOS Xcode 12 runners have been terminally slow for some time now. Looking at the setup time, it took nearly 9 minutes to build QuTiP, which is crazy compared to Linux and Windows build times (~2 minutes), or even Xcode 11 (~3 minutes).

We noticed this starting several months ago, but the problem is that I don't have a machine I can test it on to see if it's a Travis problem or an Xcode 12 one. I'm unwilling to change my personal macOS version beyond 10.14 in part because there are changes to how C compilation and debugging are handled - Apple are making it harder and harder to attach debuggers and set up sane compilers that aren't Xcode, and it took me long enough to get an actually fully featured compiler running on 10.14, let alone the next versions. Their vendored version of clang doesn't even support OpenMP!

@jakelishman

Copy link
Copy Markdown
Member Author

@hodgestar how's the setup.py file look to you, Simon? That's the biggest change from the other version of this PR - I tidied it all up into separated functions as I think you mentioned in that review.

@hodgestar

Copy link
Copy Markdown
Contributor

The macOS Xcode 12 runners have been terminally slow for some time now. Looking at the setup time, it took nearly 9 minutes to build QuTiP, which is crazy compared to Linux and Windows build times (~2 minutes), or even Xcode 11 (~3 minutes).

If it's a known issue, let's leave it for when we switch CI to GitHub Actions (assuming that's the choice we end up making).

@hodgestar

Copy link
Copy Markdown
Contributor

@hodgestar how's the setup.py file look to you, Simon? That's the biggest change from the other version of this PR - I tidied it all up into separated functions as I think you mentioned in that review.

I noticed! It looks great. :D

@jakelishman

Copy link
Copy Markdown
Member Author

If it's a known issue, let's leave it for when we switch CI to GitHub Actions (assuming that's the choice we end up making).

It's very very difficult to tell, because Travis seem to be aggressively winding down the processing power available to us on Macs - I can't tell if there's a problem in this code, if Travis are over-stuffing their Xcode 12 machines with too many VMs, or even if it's some weird emulation (I think Azure emulates PPC architectures if you ask for them!).

@jakelishman

Copy link
Copy Markdown
Member Author

So I re-ran the failed macOS Xcode 12 test, and it succeeded no problems this time round, taking half the previous amount of time to build the library. I suspect Travis' Macs are just being intermittently overloaded.

@nathanshammah nathanshammah left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @jakelishman. I added some suggestions on the way this can be triggered. E.g., with a label. Another option is to have the creation of GH release be the trigger event, see the workflow for Mitiq.

Comment thread .github/workflows/build.yml Outdated
Comment thread .github/workflows/build.yml Outdated
workflow_dispatch:
inputs:
confirm_ref:
description: "Enter branch/tag name to deploy to PyPI, or leave blank to only build wheels."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can a check on github.repository or github.repository_owner work as well? See related PR for Mitiq unitaryfoundation/mitiq#597

Comment thread .github/workflows/build.yml Outdated
workflow_dispatch:
inputs:
confirm_ref:
description: "Enter branch/tag name to deploy to PyPI, or leave blank to only build wheels."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think that making the trigger based on a label ("PyPI release") would be nice.

Comment thread .github/workflows/build.yml Outdated
Comment thread setup.cfg
@BoxiLi

BoxiLi commented Mar 24, 2021

Copy link
Copy Markdown
Member

About tests being very slow, I saw the following notice when I open the details:

Please be aware travis-ci.org will be shutting down in several weeks, with all accounts migrating to travis-ci.com. Please stay tuned here for more information.

I think we are still on travis-ci.org? Is this related to us? Maybe "switch CI to GitHub Actions" is more pressing than we thought.

@jakelishman

Copy link
Copy Markdown
Member Author

I think we are still on travis-ci.org? Is this related to us? Maybe "switch CI to GitHub Actions" is more pressing than we thought.

Yeah, it's one of the reasons that issue has made a fairly huge resurgence recently. We're absolutely running on borrowed time on Travis.

@jakelishman

jakelishman commented Mar 25, 2021

Copy link
Copy Markdown
Member Author

Last review re-request, hopefully. I think everything's correct and ready now.

  1. I had forgotten to build the sdist here, so it would have been missing from the pip package. That's fixed now.
  2. It's harder than you might expect to quickly fail all jobs in a GitHub Actions workflow right now. It seems that they expect you to try and continue for as long as possible for jobs on different runners, so I had to make everything depend on the confirmation verification to get the fail-fast behaviour.
  3. Hopefully neatened up the language we've been talking about.
  4. Removed installation of unnecessary VC++ tools for Python 2.7 on Windows.

Comment thread .github/workflows/build.yml Outdated
Comment thread .github/workflows/build.yml Outdated

@hodgestar hodgestar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I made two small suggestions to the echo'ed messages to clarify what "deploy" means and that we're uploading more than just wheels, but happy for this to be merged.

Now that wheels we be built from CI, and we will want to release direct
to PyPI/conda from there, it's more useful to have a single file that
can be simply read and parsed by the CI shell.  This also means that
release versioning changes need only affect a single file, and this file
can be populated from git tag or similar.

The "ISRELEASED" flag is now moved to either a commandline argument to
setup.py (--release), or by setting the environment variable
    CI_QUTIP_RELEASE
to be non-empty.  The environment variable strategy is better for CI
(hence the name), where tools like cibuildwheel can't pass on
command-line options to our setup.py.

We still create a git hash if not in release mode and git is available
(or add '+nogit' if not), in order to distinguish between release builds
and local versions in bugfixing.
Wheels are automatically built on all pushes to the default branch
(including PR merges), and can be manually triggered from the GitHub
Actions web interface.

Use Python package cibuildwheel to build wheels for Linux (manylinux*),
macOS and Windows.
Separate out most packaging data into the relevant new configuration
files, rather than mixing our data and code together in setup.py.  We
still rely on setuptools, and some parts of our Cython infrastructure
and versioning system still rely on us executing the setup.py file, but
with the additional data in pyproject.toml and setup.cfg, this can now
be handled in a more abstract manner by a range of build tools.

Cython is required at install time here, but we also make sure to
distribute the .cpp files with the source; in theory it should be
possible to build QuTiP without Cython as long as we do that, though
right now we're not set up for it.

We swap to package- and module-discovery techniques rather than
enumeration; it simplifies development of new packages a little, and
should be less error-prone.

You can now do
    pip install -e .
_or_
    python setup.py develop
in the git root to install QuTiP in editable mode.  The former will work
as long as a suitable Python and pip are available; all build
dependencies will be fulfilled during the run.

However,
    pip install -e .
will install build dependencies in a temporary venv, which typically
causes Cython to re-cythonise all .pyx and .pxd files, regardless of
whether they are out-of-date.  See, e.g.:
    https://discuss.python.org/t/pep-518-and-editable-mode-dont-install-already-satisfied-dependencies/3124/9
This may be improved in a future version of Cython.
Nowadays, we expect Visual C++ tools as the compiler on Windows, no
matter what version of Python is being used.
This is the old name of the new NPY_ARRAY_OWNDATA flag.
We only want to build in extra sources to the modules that need them, in
order to keep compiled code size down as low as possible.
There doesn't appear to be a reason to prefer an old version of Ubuntu
here, particularly as most of the build is done within a Docker image
anyway.
We don't build Python 2.7 wheels, so we don't need the Python 2.7 build
tools.  Normally you need some special headers and considerations on
Windows because the Python 2.7 headers use C++ features that the VC++
never implemented properly.
Previously the build would continue, just with a sentinel to show that
the deployment would not occur.  This was to allow the case when you
just wanted to build the wheels.

Now, you get the same behaviour if you don't enter anything.  If you
enter something, but it _doesn't_ match, then the build will fail
immediately.  This should just be a little easier, since you won't have
to manually cancel it if you get it wrong.

We have to have a slightly unusual dependency graph for our jobs
(everything depends on deploy_test) in order to fail as quickly as
possible.  As of 2021-03, GitHub Actions doesn't have a simpler method
of cancelling jobs that are executing on other runners, so this is the
cleanest solution without making external API calls.
Tarballs are the only official sdist in PEP 517, but for ease of use for
Windows users we still want to build and distribute zipfiles, like we
have done in the past.
@jakelishman

Copy link
Copy Markdown
Member Author

Ok, it's taken about 2 months of us deciding on what exactly we want, but I think we're better off for it. Everything's passing, so I'm merging this now.

@jakelishman jakelishman merged commit 8009919 into qutip:master Mar 27, 2021
@jakelishman jakelishman deleted the packaging-master branch March 27, 2021 12:49
jakelishman added a commit to jakelishman/qutip-feedstock that referenced this pull request Apr 11, 2021
The first version of this release didn't set the new CI_QUTIP_RELEASE
environment variable during the build process, and consequently the
distributed version of QuTiP reported its version as `4.6.0+nogit`.  pip
versions were correct since those wheels were built on GitHub Actions
the correct way.

See qutip/qutip#1465
@jakelishman jakelishman added this to the QuTiP 4.6 milestone Jun 24, 2021
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.

5 participants