improve PyQt install

- use a single script for all PyQt versions
- add hashes
- add a new ./run-qt5.14 script for testing with PyQt5.14
This commit is contained in:
Damien Elmes 2021-10-23 10:56:17 +10:00
parent 70292f07a6
commit a14eb6a1e8
23 changed files with 346 additions and 413 deletions

View file

@ -7,8 +7,7 @@ load("//proto:protobuf.bzl", "setup_protobuf_binary")
load("//proto:clang_format.bzl", "setup_clang_format") load("//proto:clang_format.bzl", "setup_clang_format")
load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install") load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")
load("@io_bazel_rules_sass//:defs.bzl", "sass_repositories") load("@io_bazel_rules_sass//:defs.bzl", "sass_repositories")
load("//python/pyqt5:defs.bzl", "install_pyqt5") load("//python/pyqt:defs.bzl", "install_pyqt")
load("//python/pyqt6:defs.bzl", "install_pyqt6")
load("@rules_python//python:pip.bzl", "pip_parse") load("@rules_python//python:pip.bzl", "pip_parse")
anki_version = "2.1.49" anki_version = "2.1.49"
@ -40,14 +39,22 @@ def setup_deps():
extra_pip_args = ["--require-hashes"], extra_pip_args = ["--require-hashes"],
) )
install_pyqt5( install_pyqt(
name = "pyqt5",
python_runtime = "@python//:python",
)
install_pyqt6(
name = "pyqt6", name = "pyqt6",
python_runtime = "@python//:python", python_runtime = "@python//:python",
requirements = "//python/pyqt:6_2/requirements.txt",
)
install_pyqt(
name = "pyqt515",
python_runtime = "@python//:python",
requirements = "//python/pyqt:5_15/requirements.txt",
)
install_pyqt(
name = "pyqt514",
python_runtime = "@python//:python",
requirements = "//python/pyqt:5_14/requirements.txt",
) )
node_repositories( node_repositories(

View file

@ -3,15 +3,7 @@ Sadly this is complicated by the fact that Python can only tell us which transit
are required by actually installing packages, and if you run pip-tools on a Mac or Linux machine, are required by actually installing packages, and if you run pip-tools on a Mac or Linux machine,
it will miss packages that are required on Windows and vice versa. it will miss packages that are required on Windows and vice versa.
So we're stuck manually merging dependencies for now. To update deps: Currently the Windows dependencies are a strict superset, so the package locks need to be generated
on a Windows machine. To do so, run "bazel run update" from this folder.
- run 'bazel run update' to update requirements.txt for the current pyqt is handled separately - see pyqt/
platform
- consult the git diff, and manually merge the changes, undoing the removal
of items pinned on other platforms
- repeat the process on the other platform
- run the tests to ensure nothing has broken on either platform
- commit the changes to requirements.txt
At the time of writing, Macs and Linux machines have identical output - it is only
Windows that differs. But we should not assume that will always be the case.

View file

@ -0,0 +1,3 @@
pyqt5==5.14.1
pyqtwebengine==5.14.0
pyqt5_sip==12.8.1

View file

@ -0,0 +1,42 @@
pyqt5==5.14.1 \
--hash=sha256:2d94ec761fb656707050c68b41958e3a9f755bb1df96c064470f4096d2899e32 \
--hash=sha256:2f230f2dbd767099de7a0cb915abdf0cbc3256a0b5bb910eb09b99117db7a65b \
--hash=sha256:31b142a868152d60c6323e0527edb692fdf05fd7cb4fe2fe9ce07d1ce560221a \
--hash=sha256:713b9a201f5e7b2fca8691373e5d5c8c2552a51d87ca9ffbb1461e34e3241211 \
--hash=sha256:a0bfe9fd718bca4de3e33000347e048f73126b6dc46530eb020b0251a638ee9d
# via
# -r requirements.in
# pyqtwebengine
pyqt5-sip==12.8.1 \
--hash=sha256:0304ca9114b9817a270f67f421355075b78ff9fc25ac58ffd72c2601109d2194 \
--hash=sha256:0cd969be528c27bbd4755bd323dff4a79a8fdda28215364e6ce3e069cb56c2a9 \
--hash=sha256:2f35e82fd7ec1e1f6716e9154721c7594956a4f5bd4f826d8c6a6453833cc2f0 \
--hash=sha256:30e944db9abee9cc757aea16906d4198129558533eb7fadbe48c5da2bd18e0bd \
--hash=sha256:34dcd29be47553d5f016ff86e89e24cbc5eebae92eb2f96fb32d2d7ba028c43c \
--hash=sha256:5a011aeff89660622a6d5c3388d55a9d76932f3b82c95e82fc31abd8b1d2990d \
--hash=sha256:6c1ebee60f1d2b3c70aff866b7933d8d8d7646011f7c32f9321ee88c290aa4f9 \
--hash=sha256:7b81382ce188d63890a0e35abe0f9bb946cabc873a31873b73583b0fc84ac115 \
--hash=sha256:832fd60a264de4134c2824d393320838f3ab648180c9c357ec58a74524d24507 \
--hash=sha256:84ba7746762bd223bed22428e8561aa267a229c28344c2d28c5d5d3f8970cffb \
--hash=sha256:9312ec47cac4e33c11503bc1cbeeb0bdae619620472f38e2078c5a51020a930f \
--hash=sha256:a1b8ef013086e224b8e86c93f880f776d01b59195bdfa2a8e0b23f0480678fec \
--hash=sha256:a29e2ac399429d3b7738f73e9081e50783e61ac5d29344e0802d0dcd6056c5a2 \
--hash=sha256:b6d42250baec52a5f77de64e2951d001c5501c3a2df2179f625b241cbaec3369 \
--hash=sha256:bb5a87b66fc1445915104ee97f7a20a69decb42f52803e3b0795fa17ff88226c \
--hash=sha256:c317ab1263e6417c498b81f5c970a9b1af7acefab1f80b4cc0f2f8e661f29fc5 \
--hash=sha256:c9800729badcb247765e4ffe2241549d02da1fa435b9db224845bc37c3e99cb0 \
--hash=sha256:c9d6d448c29dc6606bb7974696608f81f4316c8234f7c7216396ed110075e777 \
--hash=sha256:da9c9f1e65b9d09e73bd75befc82961b6b61b5a3b9d0a7c832168e1415f163c6 \
--hash=sha256:ed897c58acf4a3cdca61469daa31fe6e44c33c6c06a37c3f21fab31780b3b86a \
--hash=sha256:f168f0a7f32b81bfeffdf003c36f25d81c97dee5eb67072a5183e761fe250f13
# via
# -r requirements.in
# pyqt5
# pyqtwebengine
pyqtwebengine==5.14.0 \
--hash=sha256:01cd7f38ba4efa5f4c0983219ab15dad7747a0ca9378c7832a3077a53988f5ea \
--hash=sha256:37c4a820c5bcc82a6cb43ad33b8c81eee4c4772fc03e180a8fa37a59f99f6a48 \
--hash=sha256:3d0cba04f64d4f66087cc92e254ff8b33ec4a4e6c7751417fe2bd53c3ed740a7 \
--hash=sha256:85e1fac1b2c9bebf0b2e8cd9a75c14a38aad75165a8d8bcb8f6318944b779b25 \
--hash=sha256:e11595051f8bfbfa49175d899b2c8c2eea3a3deac4141edf4db68c3555221c92
# via -r requirements.in

View file

@ -0,0 +1,3 @@
pyqt5==5.15.2
pyqtwebengine==5.15.2
pyqt5_sip==12.8.1

View file

@ -0,0 +1,42 @@
pyqt5==5.15.2 \
--hash=sha256:29889845688a54d62820585ad5b2e0200a36b304ff3d7a555e95599f110ba4ce \
--hash=sha256:372b08dc9321d1201e4690182697c5e7ffb2e0770e6b4a45519025134b12e4fc \
--hash=sha256:894ca4ae767a8d6cf5903784b71f755073c78cb8c167eecf6e4ed6b3b055ac6a \
--hash=sha256:ea24f24b7679bf393dd2e4f53fe0ce65021be18304c1ff7a226c2fc5c356d0da \
--hash=sha256:faaecb76ec65e12673a968e7f5bc02495957e6996f0a3fa0d98895f9e4113746
# via
# -r requirements.in
# pyqtwebengine
pyqt5-sip==12.8.1 \
--hash=sha256:0304ca9114b9817a270f67f421355075b78ff9fc25ac58ffd72c2601109d2194 \
--hash=sha256:0cd969be528c27bbd4755bd323dff4a79a8fdda28215364e6ce3e069cb56c2a9 \
--hash=sha256:2f35e82fd7ec1e1f6716e9154721c7594956a4f5bd4f826d8c6a6453833cc2f0 \
--hash=sha256:30e944db9abee9cc757aea16906d4198129558533eb7fadbe48c5da2bd18e0bd \
--hash=sha256:34dcd29be47553d5f016ff86e89e24cbc5eebae92eb2f96fb32d2d7ba028c43c \
--hash=sha256:5a011aeff89660622a6d5c3388d55a9d76932f3b82c95e82fc31abd8b1d2990d \
--hash=sha256:6c1ebee60f1d2b3c70aff866b7933d8d8d7646011f7c32f9321ee88c290aa4f9 \
--hash=sha256:7b81382ce188d63890a0e35abe0f9bb946cabc873a31873b73583b0fc84ac115 \
--hash=sha256:832fd60a264de4134c2824d393320838f3ab648180c9c357ec58a74524d24507 \
--hash=sha256:84ba7746762bd223bed22428e8561aa267a229c28344c2d28c5d5d3f8970cffb \
--hash=sha256:9312ec47cac4e33c11503bc1cbeeb0bdae619620472f38e2078c5a51020a930f \
--hash=sha256:a1b8ef013086e224b8e86c93f880f776d01b59195bdfa2a8e0b23f0480678fec \
--hash=sha256:a29e2ac399429d3b7738f73e9081e50783e61ac5d29344e0802d0dcd6056c5a2 \
--hash=sha256:b6d42250baec52a5f77de64e2951d001c5501c3a2df2179f625b241cbaec3369 \
--hash=sha256:bb5a87b66fc1445915104ee97f7a20a69decb42f52803e3b0795fa17ff88226c \
--hash=sha256:c317ab1263e6417c498b81f5c970a9b1af7acefab1f80b4cc0f2f8e661f29fc5 \
--hash=sha256:c9800729badcb247765e4ffe2241549d02da1fa435b9db224845bc37c3e99cb0 \
--hash=sha256:c9d6d448c29dc6606bb7974696608f81f4316c8234f7c7216396ed110075e777 \
--hash=sha256:da9c9f1e65b9d09e73bd75befc82961b6b61b5a3b9d0a7c832168e1415f163c6 \
--hash=sha256:ed897c58acf4a3cdca61469daa31fe6e44c33c6c06a37c3f21fab31780b3b86a \
--hash=sha256:f168f0a7f32b81bfeffdf003c36f25d81c97dee5eb67072a5183e761fe250f13
# via
# -r requirements.in
# pyqt5
# pyqtwebengine
pyqtwebengine==5.15.2 \
--hash=sha256:4d72fea774071ce6f76e341a3d2c5d595886c9906a9b9493239c841cce54a634 \
--hash=sha256:8ba247d0873bbeaee70f273cafbfa985a93947b2b8df23059607ec3595fee128 \
--hash=sha256:db99bdf6c01c84bcc7369b0c49702e40451bc48372d4281777e17dbb84874c1a \
--hash=sha256:e9f4a09cce6d8ab8b6c7e7a11e21f606ae713ff275c749839aa4d871a799fccc \
--hash=sha256:fe5a02d4cf2327b6a930f8146589842b7664ac703cfddf1f83172423a32e3164
# via -r requirements.in

View file

@ -0,0 +1,5 @@
pyqt6==6.2.0
pyqt6-qt6==6.2.0
pyqt6-webengine==6.2.0
pyqt6-webengine-qt6==6.2.0
pyqt6_sip==13.1.0

View file

@ -0,0 +1,51 @@
pyqt6==6.2.0 \
--hash=sha256:142ce7fa574d7ebb13fb0a2ebd18c86087c35829f786c094a71a0749155d8fee \
--hash=sha256:8d883b66d2b902862b90ff51696e408aa618f1e4e9d392c98f13925b5983068b \
--hash=sha256:c43f4bb7dbe58b262b88da137de54a140091a5273b3333406a76507551c34321 \
--hash=sha256:ea4c1502214eed9090937f46ed33aca054ddc46240d6c5ce09886d1d6886b50e
# via
# -r requirements.in
# pyqt6-webengine
pyqt6-qt6==6.2.0 \
--hash=sha256:7749e44ab5ca98aba766b283bcd08ebeb0ffb09e427efc7beac0c571525f3c5e \
--hash=sha256:806dc7289614f08e1fa28ac5abbbbf2d61305a416b8183045b9c535321c7309c \
--hash=sha256:c0d4c1b1b7bd0c9f21a6284d6aa53c041cb36dc464acb403cbe1bc8845090d30 \
--hash=sha256:ed1f73e40deddadb10781ddc40672e14c2e7309ca389263c081ed702267451c7
# via
# -r requirements.in
# pyqt6
pyqt6-sip==13.1.0 \
--hash=sha256:04c5f6dd0c5be27f27be286e500cf1dd718b9e00b735b88b5e2ada74d86326e6 \
--hash=sha256:1afbba8d83a55164e150e04f8ed52a3a5292a347ddaecbc6f0b819fadccdb176 \
--hash=sha256:48fab3bc4121d77c081102ad074f63deeae4736b1a88dd19fe05364421f28376 \
--hash=sha256:5d5001c1ed83b0b9946f5a2b9a9b27f9631dc6613306f88f3946437205d176be \
--hash=sha256:6f0c3fa80b81bb28701772b9d89e11fe3677591048b18d50224bea0138063597 \
--hash=sha256:7c31073fe8e6cb8a42e85d60d3a096700a9047c772b354d6227dfe965566ec8a \
--hash=sha256:a892f66d506a7adc40c03d95ef54c152614f32c2975f534cd9deb44ca94d1124 \
--hash=sha256:c20fdbc8f50052242c84680d9cf1580dd815b2d55ae73e71885864b0320b3585 \
--hash=sha256:c6d017380c0a3e8ef94f6eecc119a056ec3e6f71c9c5b7957a1c2dc51007901f \
--hash=sha256:c6e1864f0018bb2e27a42a32018fe298790bac1e835bc2a699f341b51c884e7b \
--hash=sha256:cbc4ee1997c029d84c2f5ac8ab10089943d93e7b5eb9399a967b93969127c61d \
--hash=sha256:d286186990c2180d2b631660b5eaee202bbba031b87b73272d7b7c2ae1c4d001 \
--hash=sha256:da4742ad9a983dd384a28d743dd14c47de4842ab476c90a36e8f261998cf83ec \
--hash=sha256:ee7e635aadc3d3baaeb8ec509e562d7d6cc5c6d738cb874186f076742a2aec27 \
--hash=sha256:f36c6c73137b835024f0bcefe17b74a1fcacd759ebf1a01460f353ced1c34a30 \
--hash=sha256:f591414ea4e3029a4873286496b6685d6a260249f0375657c1043e4db5a5514c
# via
# -r requirements.in
# pyqt6
# pyqt6-webengine
pyqt6-webengine==6.2.0 \
--hash=sha256:3df64503bcd80e529181ed14993d39469611bf2cd0b81a4959a5f258020cc06e \
--hash=sha256:4f12a984efd01d202a89baea3437c6fb2001a042f9bdef512d324eb4e81ef693 \
--hash=sha256:7a27c567c8ffe17ec52687469b90465330e33deb9856f7de6f269b8d75101704 \
--hash=sha256:9df1479df84b7857c86bc233d66ec50edcee7c5e38c11ff5511e0006763bc7e4
# via -r requirements.in
pyqt6-webengine-qt6==6.2.0 \
--hash=sha256:310cb5e5e18b0ae380b2c3b111657cccec1cd89d5e380460baa34d5bacad24e0 \
--hash=sha256:9940aa76488bc2cd6ae924be5dc111fcafbe960e70571f274fa6e5583272ae6d \
--hash=sha256:d03f68c9a12b715656c8539b38c8d0a106f4462c96344528beb6d9797b1ee4fb \
--hash=sha256:ea204703f25ba93a929138860a80f26530057478510e6e0ff526d3279abc6505
# via
# -r requirements.in
# pyqt6-webengine

10
python/pyqt/README.md Normal file
View file

@ -0,0 +1,10 @@
We handle PyQt specially for a few reasons:
- It ships with a few issues in its .pyi files that we want to correct.
- Bazel's Python tools install each package in a separate folder, but the
various PyQt packages expect to be installed in the same location, and
will give runtime linking issues when they are split up.
To update the version lock file, change to the folder with requirements.in,
modify the file, and then run "bazel run //python:update". PyQt does not
depend on platform-specific packages, so the update can be run on any platform.

View file

@ -3,7 +3,7 @@
def _execute(repository_ctx, arguments, quiet = False): def _execute(repository_ctx, arguments, quiet = False):
return repository_ctx.execute(arguments, environment = {}, quiet = quiet) return repository_ctx.execute(arguments, environment = {}, quiet = quiet)
def _install_pyqt5_impl(repository_ctx): def _install_pyqt_impl(repository_ctx):
python_interpreter = repository_ctx.attr.python_interpreter python_interpreter = repository_ctx.attr.python_interpreter
if repository_ctx.attr.python_runtime: if repository_ctx.attr.python_runtime:
python_interpreter = repository_ctx.path(repository_ctx.attr.python_runtime) python_interpreter = repository_ctx.path(repository_ctx.attr.python_runtime)
@ -12,13 +12,14 @@ def _install_pyqt5_impl(repository_ctx):
python_interpreter, python_interpreter,
repository_ctx.path(repository_ctx.attr._script), repository_ctx.path(repository_ctx.attr._script),
repository_ctx.path("."), repository_ctx.path("."),
repository_ctx.path(repository_ctx.attr.requirements),
] ]
result = _execute(repository_ctx, args, quiet = repository_ctx.attr.quiet) result = _execute(repository_ctx, args, quiet = repository_ctx.attr.quiet)
if result.return_code: if result.return_code:
fail("failed: %s (%s)" % (result.stdout, result.stderr)) fail("failed: %s (%s)" % (result.stdout, result.stderr))
install_pyqt5 = repository_rule( install_pyqt = repository_rule(
attrs = { attrs = {
"python_interpreter": attr.string(default = "python", doc = """ "python_interpreter": attr.string(default = "python", doc = """
The command to run the Python interpreter used to invoke pip and unpack the The command to run the Python interpreter used to invoke pip and unpack the
@ -30,13 +31,14 @@ If the label is specified it will overwrite the python_interpreter attribute.
"""), """),
"_script": attr.label( "_script": attr.label(
executable = True, executable = True,
default = Label("//python/pyqt5:install_pyqt5.py"), default = Label("//python/pyqt:install.py"),
cfg = "host", cfg = "host",
), ),
"requirements": attr.label(allow_files = True),
"quiet": attr.bool( "quiet": attr.bool(
default = True, default = True,
doc = "If stdout and stderr should be printed to the terminal.", doc = "If stdout and stderr should be printed to the terminal.",
), ),
}, },
implementation = _install_pyqt5_impl, implementation = _install_pyqt_impl,
) )

127
python/pyqt/install.py Normal file
View file

@ -0,0 +1,127 @@
# based on https://github.com/ali5h/rules_pip/blob/master/src/whl.py
# MIT
import os
import platform
import subprocess
import sys
from pip._internal.commands import create_command
def install_packages(requirements_path, directory, pip_args):
pip_args = [
"--isolated",
"--disable-pip-version-check",
"--target",
directory,
"--no-deps",
"--ignore-requires-python",
"--require-hashes",
"-r",
requirements_path
] + pip_args
cmd = create_command("install")
cmd.main(pip_args)
def fix_pyi_types():
"Fix broken PyQt types."
for dirpath, dirnames, fnames in os.walk("."):
for fname in fnames:
if not fname.endswith(".pyi"):
continue
path = os.path.join(dirpath, fname)
with open(path, "r+") as file:
lines = file.readlines()
file.seek(0)
for line in lines:
# inheriting from the missing sip.sipwrapper definition
# causes missing attributes not to be detected, as it's treating
# the class as inheriting from Any
line = line.replace("PyQt6.sip.wrapper", "object")
# # remove blanket getattr in QObject which also causes missing
# # attributes not to be detected
if "def __getattr__(self, name: str) -> typing.Any" in line:
continue
file.write(line + "\n")
def fix_webengine_codesigning():
"Fix a codesigning issue in the 6.2.0 release."
path = "PyQt6/Qt6/lib/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess"
if os.path.exists(path):
subprocess.run(["codesign", "-s", "-", path], check=True)
def main():
base = sys.argv[1]
requirements_file = sys.argv[2]
# has user told us to use a custom existing folder instead?
if local_site_packages := os.environ.get("PYTHON_SITE_PACKAGES"):
subprocess.run(
[
"rsync",
"-ai",
"--include=PyQt**",
"--exclude=*",
local_site_packages,
base + "/",
],
check=True,
)
with open(os.path.join(base, "__init__.py"), "w") as file:
pass
else:
arm_darwin = sys.platform.startswith("darwin") and platform.machine() == "arm64"
pip_args = []
if arm_darwin:
# pyqt messed up the architecture tags in the 6.2.0 release
pip_args.extend(
[
"--platform=macosx_10_14_arm64",
"--only-binary=pyqt6-qt6,pyqt6-webengine-qt6",
])
install_packages(requirements_file, base, pip_args)
fix_pyi_types()
fix_webengine_codesigning()
with open(os.path.join(base, "__init__.py"), "w") as file:
file.write("__path__ = __import__('pkgutil').extend_path(__path__, __name__)")
pkg_name = os.path.basename(base)
result = """
load("@rules_python//python:defs.bzl", "py_library")
package(default_visibility = ["//visibility:public"])
py_library(
name = "{}",
srcs = glob(["**/*.py"]),
data = glob(["**/*"], exclude = [
"**/*.py",
"**/*.pyc",
"**/* *",
"BUILD",
"WORKSPACE",
"bin/*",
"__pycache__",
# these make building slower
"Qt/qml/**",
"**/*.sip",
"**/*.png",
]),
# This makes this directory a top-level in the python import
# search path for anything that depends on this.
imports = ["."],
)
""".format(pkg_name)
with open(os.path.join(base, "BUILD"), "w") as f:
f.write(result)
if __name__ == "__main__":
main()

View file

@ -1,157 +0,0 @@
# based on https://github.com/ali5h/rules_pip/blob/master/src/whl.py
# MIT
"""downloads and parses info of a pkg and generates a BUILD file for it"""
import glob
import os
import re
import shutil
import subprocess
import sys
from pip._internal.commands import create_command
def install_package(pkg, directory, pip_args):
"""Downloads wheel for a package. Assumes python binary provided has
pip and wheel package installed.
Args:
pkg: package name
directory: destination directory to download the wheel file in
python: python binary path used to run pip command
pip_args: extra pip args sent to pip
Returns:
str: path to the wheel file
"""
pip_args = [
"--isolated",
"--disable-pip-version-check",
"--target",
directory,
"--no-deps",
"--ignore-requires-python",
pkg,
] + pip_args
cmd = create_command("install")
cmd.main(pip_args)
def _cleanup(directory, pattern):
for p in glob.glob(os.path.join(directory, pattern)):
shutil.rmtree(p)
fix_none = re.compile(r"(\s*None) =")
def copy_and_fix_pyi(source, dest):
"Fix broken PyQt types."
with open(source) as input_file:
with open(dest, "w") as output_file:
for line in input_file.readlines():
# assigning to None is a syntax error
line = fix_none.sub(r"\1_ =", line)
# inheriting from the missing sip.sipwrapper definition
# causes missing attributes not to be detected, as it's treating
# the class as inheriting from Any
line = line.replace("sip.simplewrapper", "object")
line = line.replace("sip.wrapper", "object")
# remove blanket getattr in QObject which also causes missing
# attributes not to be detected
if "def __getattr__(self, name: str) -> typing.Any" in line:
continue
output_file.write(line)
def merge_files(root, source):
for dirpath, _dirnames, filenames in os.walk(source):
target_dir = os.path.join(root, os.path.relpath(dirpath, source))
if not os.path.exists(target_dir):
os.mkdir(target_dir)
for fname in filenames:
source_path = os.path.join(dirpath, fname)
target_path = os.path.join(target_dir, fname)
if not os.path.exists(target_path):
if fname.endswith(".pyi"):
copy_and_fix_pyi(source_path, target_path)
else:
shutil.copy2(source_path, target_path)
def main():
base = sys.argv[1]
local_site_packages = os.environ.get("PYTHON_SITE_PACKAGES")
if local_site_packages:
subprocess.run(
[
"rsync",
"-ai",
"--include=PyQt**",
"--exclude=*",
local_site_packages,
base + "/",
],
check=True,
)
with open(os.path.join(base, "__init__.py"), "w") as file:
pass
else:
packages = [
("pyqt5", "pyqt5==5.15.2"),
("pyqtwebengine", "pyqtwebengine==5.15.2"),
("pyqt5-sip", "pyqt5_sip==12.8.1"),
]
for (name, with_version) in packages:
# install package in subfolder
folder = os.path.join(base, "temp")
install_package(with_version, folder, [])
# merge into parent
merge_files(base, folder)
shutil.rmtree(folder)
with open(os.path.join(base, "__init__.py"), "w") as file:
file.write("__path__ = __import__('pkgutil').extend_path(__path__, __name__)")
# add missing py.typed file
with open(os.path.join(base, "py.typed"), "w") as file:
pass
result = """
load("@rules_python//python:defs.bzl", "py_library")
package(default_visibility = ["//visibility:public"])
py_library(
name = "pkg",
srcs = glob(["**/*.py"]),
data = glob(["**/*"], exclude = [
"**/*.py",
"**/*.pyc",
"**/* *",
"BUILD",
"WORKSPACE",
"bin/*",
"__pycache__",
# these make building slower
"Qt/qml/**",
"**/*.sip",
"**/*.png",
]),
# This makes this directory a top-level in the python import
# search path for anything that depends on this.
imports = ["."],
)
"""
# clean up
_cleanup(base, "__pycache__")
with open(os.path.join(base, "BUILD"), "w") as f:
f.write(result)
if __name__ == "__main__":
main()

View file

@ -1,42 +0,0 @@
# based off https://github.com/ali5h/rules_pip/blob/master/defs.bzl
def _execute(repository_ctx, arguments, quiet = False):
return repository_ctx.execute(arguments, environment = {}, quiet = quiet)
def _install_pyqt6_impl(repository_ctx):
python_interpreter = repository_ctx.attr.python_interpreter
if repository_ctx.attr.python_runtime:
python_interpreter = repository_ctx.path(repository_ctx.attr.python_runtime)
args = [
python_interpreter,
repository_ctx.path(repository_ctx.attr._script),
repository_ctx.path("."),
]
result = _execute(repository_ctx, args, quiet = repository_ctx.attr.quiet)
if result.return_code:
fail("failed: %s (%s)" % (result.stdout, result.stderr))
install_pyqt6 = repository_rule(
attrs = {
"python_interpreter": attr.string(default = "python", doc = """
The command to run the Python interpreter used to invoke pip and unpack the
wheels.
"""),
"python_runtime": attr.label(doc = """
The label to the Python run-time interpreted used to invoke pip and unpack the wheels.
If the label is specified it will overwrite the python_interpreter attribute.
"""),
"_script": attr.label(
executable = True,
default = Label("//python/pyqt6:install_pyqt6.py"),
cfg = "host",
),
"quiet": attr.bool(
default = True,
doc = "If stdout and stderr should be printed to the terminal.",
),
},
implementation = _install_pyqt6_impl,
)

View file

@ -1,167 +0,0 @@
# based on https://github.com/ali5h/rules_pip/blob/master/src/whl.py
# MIT
"""downloads and parses info of a pkg and generates a BUILD file for it"""
import glob
import os
import re
import shutil
import subprocess
import sys
import platform
from pip._internal.commands import create_command
def install_package(pkg, directory, pip_args):
"""Downloads wheel for a package. Assumes python binary provided has
pip and wheel package installed.
Args:
pkg: package name
directory: destination directory to download the wheel file in
python: python binary path used to run pip command
pip_args: extra pip args sent to pip
Returns:
str: path to the wheel file
"""
pip_args = [
"--isolated",
"--disable-pip-version-check",
"--target",
directory,
"--no-deps",
"--ignore-requires-python",
pkg,
] + pip_args
cmd = create_command("install")
cmd.main(pip_args)
def _cleanup(directory, pattern):
for p in glob.glob(os.path.join(directory, pattern)):
shutil.rmtree(p)
def copy_and_fix_pyi(source, dest):
"Fix broken PyQt types."
with open(source) as input_file:
with open(dest, "w") as output_file:
for line in input_file.readlines():
# inheriting from the missing sip.sipwrapper definition
# causes missing attributes not to be detected, as it's treating
# the class as inheriting from Any
line = line.replace("PyQt6.sip.wrapper", "object")
# # remove blanket getattr in QObject which also causes missing
# # attributes not to be detected
if "def __getattr__(self, name: str) -> typing.Any" in line:
continue
output_file.write(line)
def merge_files(root, source):
for dirpath, _dirnames, filenames in os.walk(source):
target_dir = os.path.join(root, os.path.relpath(dirpath, source))
if not os.path.exists(target_dir):
os.mkdir(target_dir)
for fname in filenames:
source_path = os.path.join(dirpath, fname)
target_path = os.path.join(target_dir, fname)
if not os.path.exists(target_path):
if fname.endswith(".pyi"):
copy_and_fix_pyi(source_path, target_path)
else:
shutil.copy2(source_path, target_path)
def fix_webengine_codesigning(base: str):
"Fix a codesigning issue in the 6.2.0 release."
path = os.path.join(base, "PyQt6/Qt6/lib/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess")
subprocess.run(["codesign", "-s", "-", path], check=True)
def main():
base = sys.argv[1]
local_site_packages = os.environ.get("PYTHON_SITE_PACKAGES")
if local_site_packages:
subprocess.run(
[
"rsync",
"-ai",
"--include=PyQt**",
"--exclude=*",
local_site_packages,
base + "/",
],
check=True,
)
with open(os.path.join(base, "__init__.py"), "w") as file:
pass
else:
packages = [
("pyqt6", "pyqt6==6.2.0"),
("pyqt6-qt6", "pyqt6-qt6==6.2.0"),
("pyqt6-webengine", "pyqt6-webengine==6.2.0"),
("pyqt6-webengine-qt6", "pyqt6-webengine-qt6==6.2.0"),
("pyqt6-sip", "pyqt6_sip==13.1.0"),
]
arm_darwin = sys.platform.startswith("darwin") and platform.machine() == "arm64"
for (name, with_version) in packages:
# install package in subfolder
folder = os.path.join(base, "temp")
pip_args = []
if arm_darwin:
if name in ("pyqt6-qt6", "pyqt6-webengine-qt6"):
# pyqt messed up the architecture tags
pip_args.extend(
[
"--platform=macosx_10_14_arm64",
"--only-binary=:all:",
])
install_package(with_version, folder, pip_args)
# merge into parent
merge_files(base, folder)
shutil.rmtree(folder)
with open(os.path.join(base, "__init__.py"), "w") as file:
file.write("__path__ = __import__('pkgutil').extend_path(__path__, __name__)")
if arm_darwin:
fix_webengine_codesigning(base)
result = """
load("@rules_python//python:defs.bzl", "py_library")
package(default_visibility = ["//visibility:public"])
py_library(
name = "pkg",
srcs = glob(["**/*.py"]),
data = glob(["**/*"], exclude = [
"**/*.py",
"**/*.pyc",
"**/* *",
"BUILD",
"WORKSPACE",
"bin/*",
"__pycache__",
# these make building slower
"Qt/qml/**",
"**/*.sip",
"**/*.png",
]),
# This makes this directory a top-level in the python import
# search path for anything that depends on this.
imports = ["."],
)
"""
# clean up
_cleanup(base, "__pycache__")
with open(os.path.join(base, "BUILD"), "w") as f:
f.write(result)
if __name__ == "__main__":
main()

View file

@ -59,7 +59,7 @@ py_test(
deps = [ deps = [
"//pylib/anki", "//pylib/anki",
"//qt/aqt:aqt_without_data", "//qt/aqt:aqt_without_data",
"@pyqt6//:pkg", "@pyqt6",
requirement("mypy"), requirement("mypy"),
], ],
) )
@ -81,7 +81,7 @@ py_test(
"//pylib/anki", "//pylib/anki",
"//qt/aqt:aqt_without_data", "//qt/aqt:aqt_without_data",
requirement("pylint"), requirement("pylint"),
"@pyqt6//:pkg", "@pyqt6",
], ],
) )
@ -130,11 +130,12 @@ py_binary(
deps = [ deps = [
"//pylib/anki", "//pylib/anki",
"//qt/aqt:aqt_with_data", "//qt/aqt:aqt_with_data",
"@pyqt6",
], ],
) )
py_binary( py_binary(
name = "runanki_qt5", name = "runanki_qt515",
srcs = [ srcs = [
"bazelfixes.py", "bazelfixes.py",
"runanki.py", "runanki.py",
@ -144,7 +145,24 @@ py_binary(
tags = ["manual"], tags = ["manual"],
deps = [ deps = [
"//pylib/anki", "//pylib/anki",
"//qt/aqt:aqt_with_data_qt5", "//qt/aqt:aqt_with_data",
"@pyqt515",
],
)
py_binary(
name = "runanki_qt514",
srcs = [
"bazelfixes.py",
"runanki.py",
],
imports = ["."],
main = "runanki.py",
tags = ["manual"],
deps = [
"//pylib/anki",
"//qt/aqt:aqt_with_data",
"@pyqt514",
], ],
) )

View file

@ -66,7 +66,7 @@ py_library(
data = aqt_core_data, data = aqt_core_data,
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = aqt_deps + [ deps = aqt_deps + [
"@pyqt6//:pkg", "@pyqt6",
], ],
) )
@ -75,20 +75,7 @@ py_library(
srcs = _py_srcs_and_forms, srcs = _py_srcs_and_forms,
data = aqt_core_data + ["//qt/aqt/data"], data = aqt_core_data + ["//qt/aqt/data"],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = aqt_deps + [ deps = aqt_deps,
"@pyqt6//:pkg",
],
)
py_library(
name = "aqt_with_data_qt5",
srcs = _py_srcs_and_forms,
data = aqt_core_data + ["//qt/aqt/data"],
tags = ["manual"],
visibility = ["//visibility:public"],
deps = aqt_deps + [
"@pyqt5//:pkg",
],
) )
py_package( py_package(

View file

@ -5,7 +5,7 @@ py_binary(
name = "build_ui", name = "build_ui",
srcs = ["build_ui.py"], srcs = ["build_ui.py"],
legacy_create_init = False, legacy_create_init = False,
deps = ["@pyqt6//:pkg"], deps = ["@pyqt6"],
) )
compile_all( compile_all(

View file

@ -49,7 +49,12 @@ def fix_run_on_macos():
if not sys.platform.startswith("darwin"): if not sys.platform.startswith("darwin"):
return return
exec_folder = os.path.dirname(sys.argv[0]) exec_folder = os.path.dirname(sys.argv[0])
qt_version = 5 if "runanki_qt5" in exec_folder else 6 if "runanki_qt515" in exec_folder:
qt_version = 515
elif "runanki_qt514" in exec_folder:
qt_version = 514
else:
qt_version = 6
pyqt_repo = os.path.join(exec_folder, f"../../../../../../../external/pyqt{qt_version}") pyqt_repo = os.path.join(exec_folder, f"../../../../../../../external/pyqt{qt_version}")
if os.path.exists(pyqt_repo): if os.path.exists(pyqt_repo):
# pyqt must point to real folder, not a symlink # pyqt must point to real folder, not a symlink

View file

@ -11,8 +11,7 @@ nonstandard_header = {
"pylib/anki/importing/supermemo_xml.py", "pylib/anki/importing/supermemo_xml.py",
"pylib/anki/statsbg.py", "pylib/anki/statsbg.py",
"pylib/tools/protoc-gen-mypy.py", "pylib/tools/protoc-gen-mypy.py",
"python/pyqt5/install_pyqt5.py", "python/pyqt/install.py",
"python/pyqt6/install_pyqt6.py",
"qt/aqt/mpv.py", "qt/aqt/mpv.py",
"qt/aqt/winpaths.py", "qt/aqt/winpaths.py",
} }

View file

@ -3,4 +3,4 @@
set -e set -e
export PYTHONWARNINGS=default export PYTHONWARNINGS=default
bazel run $BUILDARGS //qt:runanki_qt5 -- $* bazel run $BUILDARGS //qt:runanki_qt514 -- $*

6
scripts/run-qt5.15 Executable file
View file

@ -0,0 +1,6 @@
#!/bin/bash
set -e
export PYTHONWARNINGS=default
bazel run $BUILDARGS //qt:runanki_qt515 -- $*