diff --git a/defs.bzl b/defs.bzl index 43b545583..faf0f7620 100644 --- a/defs.bzl +++ b/defs.bzl @@ -7,8 +7,7 @@ load("//proto:protobuf.bzl", "setup_protobuf_binary") load("//proto:clang_format.bzl", "setup_clang_format") load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install") load("@io_bazel_rules_sass//:defs.bzl", "sass_repositories") -load("//python/pyqt5:defs.bzl", "install_pyqt5") -load("//python/pyqt6:defs.bzl", "install_pyqt6") +load("//python/pyqt:defs.bzl", "install_pyqt") load("@rules_python//python:pip.bzl", "pip_parse") anki_version = "2.1.49" @@ -40,14 +39,22 @@ def setup_deps(): extra_pip_args = ["--require-hashes"], ) - install_pyqt5( - name = "pyqt5", - python_runtime = "@python//:python", - ) - - install_pyqt6( + install_pyqt( name = "pyqt6", 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( diff --git a/python/README.md b/python/README.md index 64d222871..0b841226e 100644 --- a/python/README.md +++ b/python/README.md @@ -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, 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 - 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. +pyqt is handled separately - see pyqt/ diff --git a/python/pyqt/5_14/requirements.in b/python/pyqt/5_14/requirements.in new file mode 100644 index 000000000..0d2b2be9d --- /dev/null +++ b/python/pyqt/5_14/requirements.in @@ -0,0 +1,3 @@ +pyqt5==5.14.1 +pyqtwebengine==5.14.0 +pyqt5_sip==12.8.1 diff --git a/python/pyqt/5_14/requirements.txt b/python/pyqt/5_14/requirements.txt new file mode 100644 index 000000000..6e609f027 --- /dev/null +++ b/python/pyqt/5_14/requirements.txt @@ -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 diff --git a/python/pyqt/5_15/requirements.in b/python/pyqt/5_15/requirements.in new file mode 100644 index 000000000..e2d16d210 --- /dev/null +++ b/python/pyqt/5_15/requirements.in @@ -0,0 +1,3 @@ +pyqt5==5.15.2 +pyqtwebengine==5.15.2 +pyqt5_sip==12.8.1 diff --git a/python/pyqt/5_15/requirements.txt b/python/pyqt/5_15/requirements.txt new file mode 100644 index 000000000..97fe7e847 --- /dev/null +++ b/python/pyqt/5_15/requirements.txt @@ -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 diff --git a/python/pyqt/6_2/requirements.in b/python/pyqt/6_2/requirements.in new file mode 100644 index 000000000..e753faa6e --- /dev/null +++ b/python/pyqt/6_2/requirements.in @@ -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 diff --git a/python/pyqt/6_2/requirements.txt b/python/pyqt/6_2/requirements.txt new file mode 100644 index 000000000..ed8869210 --- /dev/null +++ b/python/pyqt/6_2/requirements.txt @@ -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 diff --git a/python/pyqt5/BUILD.bazel b/python/pyqt/BUILD.bazel similarity index 100% rename from python/pyqt5/BUILD.bazel rename to python/pyqt/BUILD.bazel diff --git a/python/pyqt/README.md b/python/pyqt/README.md new file mode 100644 index 000000000..4278caed8 --- /dev/null +++ b/python/pyqt/README.md @@ -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. diff --git a/python/pyqt5/defs.bzl b/python/pyqt/defs.bzl similarity index 82% rename from python/pyqt5/defs.bzl rename to python/pyqt/defs.bzl index 2c29480e7..442169fea 100644 --- a/python/pyqt5/defs.bzl +++ b/python/pyqt/defs.bzl @@ -3,7 +3,7 @@ def _execute(repository_ctx, arguments, quiet = False): 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 if 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, repository_ctx.path(repository_ctx.attr._script), repository_ctx.path("."), + repository_ctx.path(repository_ctx.attr.requirements), ] result = _execute(repository_ctx, args, quiet = repository_ctx.attr.quiet) if result.return_code: fail("failed: %s (%s)" % (result.stdout, result.stderr)) -install_pyqt5 = repository_rule( +install_pyqt = repository_rule( attrs = { "python_interpreter": attr.string(default = "python", doc = """ 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( executable = True, - default = Label("//python/pyqt5:install_pyqt5.py"), + default = Label("//python/pyqt:install.py"), cfg = "host", ), + "requirements": attr.label(allow_files = True), "quiet": attr.bool( default = True, doc = "If stdout and stderr should be printed to the terminal.", ), }, - implementation = _install_pyqt5_impl, + implementation = _install_pyqt_impl, ) diff --git a/python/pyqt/install.py b/python/pyqt/install.py new file mode 100644 index 000000000..66f919773 --- /dev/null +++ b/python/pyqt/install.py @@ -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() diff --git a/python/pyqt5/install_pyqt5.py b/python/pyqt5/install_pyqt5.py deleted file mode 100644 index 96f3fb29a..000000000 --- a/python/pyqt5/install_pyqt5.py +++ /dev/null @@ -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() diff --git a/python/pyqt6/BUILD.bazel b/python/pyqt6/BUILD.bazel deleted file mode 100644 index e69de29bb..000000000 diff --git a/python/pyqt6/defs.bzl b/python/pyqt6/defs.bzl deleted file mode 100644 index 70a4a8f5b..000000000 --- a/python/pyqt6/defs.bzl +++ /dev/null @@ -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, -) diff --git a/python/pyqt6/install_pyqt6.py b/python/pyqt6/install_pyqt6.py deleted file mode 100644 index 1ed1a6b15..000000000 --- a/python/pyqt6/install_pyqt6.py +++ /dev/null @@ -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() diff --git a/qt/BUILD.bazel b/qt/BUILD.bazel index 9413bc92c..be49bc6a6 100644 --- a/qt/BUILD.bazel +++ b/qt/BUILD.bazel @@ -59,7 +59,7 @@ py_test( deps = [ "//pylib/anki", "//qt/aqt:aqt_without_data", - "@pyqt6//:pkg", + "@pyqt6", requirement("mypy"), ], ) @@ -81,7 +81,7 @@ py_test( "//pylib/anki", "//qt/aqt:aqt_without_data", requirement("pylint"), - "@pyqt6//:pkg", + "@pyqt6", ], ) @@ -130,11 +130,12 @@ py_binary( deps = [ "//pylib/anki", "//qt/aqt:aqt_with_data", + "@pyqt6", ], ) py_binary( - name = "runanki_qt5", + name = "runanki_qt515", srcs = [ "bazelfixes.py", "runanki.py", @@ -144,7 +145,24 @@ py_binary( tags = ["manual"], deps = [ "//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", ], ) diff --git a/qt/aqt/BUILD.bazel b/qt/aqt/BUILD.bazel index 47718f8f4..8c488d33f 100644 --- a/qt/aqt/BUILD.bazel +++ b/qt/aqt/BUILD.bazel @@ -66,7 +66,7 @@ py_library( data = aqt_core_data, visibility = ["//visibility:public"], deps = aqt_deps + [ - "@pyqt6//:pkg", + "@pyqt6", ], ) @@ -75,20 +75,7 @@ py_library( srcs = _py_srcs_and_forms, data = aqt_core_data + ["//qt/aqt/data"], visibility = ["//visibility:public"], - 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", - ], + deps = aqt_deps, ) py_package( diff --git a/qt/aqt/forms/BUILD.bazel b/qt/aqt/forms/BUILD.bazel index 327352f92..ecbf03fa0 100644 --- a/qt/aqt/forms/BUILD.bazel +++ b/qt/aqt/forms/BUILD.bazel @@ -5,7 +5,7 @@ py_binary( name = "build_ui", srcs = ["build_ui.py"], legacy_create_init = False, - deps = ["@pyqt6//:pkg"], + deps = ["@pyqt6"], ) compile_all( diff --git a/qt/bazelfixes.py b/qt/bazelfixes.py index eb223b87f..768299443 100644 --- a/qt/bazelfixes.py +++ b/qt/bazelfixes.py @@ -49,7 +49,12 @@ def fix_run_on_macos(): if not sys.platform.startswith("darwin"): return 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}") if os.path.exists(pyqt_repo): # pyqt must point to real folder, not a symlink diff --git a/scripts/copyright_headers.py b/scripts/copyright_headers.py index 20f117417..ef4ebf672 100644 --- a/scripts/copyright_headers.py +++ b/scripts/copyright_headers.py @@ -11,8 +11,7 @@ nonstandard_header = { "pylib/anki/importing/supermemo_xml.py", "pylib/anki/statsbg.py", "pylib/tools/protoc-gen-mypy.py", - "python/pyqt5/install_pyqt5.py", - "python/pyqt6/install_pyqt6.py", + "python/pyqt/install.py", "qt/aqt/mpv.py", "qt/aqt/winpaths.py", } diff --git a/run-qt5 b/scripts/run-qt5.14 similarity index 52% rename from run-qt5 rename to scripts/run-qt5.14 index c7e9e5b37..a917b0b9f 100755 --- a/run-qt5 +++ b/scripts/run-qt5.14 @@ -3,4 +3,4 @@ set -e export PYTHONWARNINGS=default -bazel run $BUILDARGS //qt:runanki_qt5 -- $* +bazel run $BUILDARGS //qt:runanki_qt514 -- $* diff --git a/scripts/run-qt5.15 b/scripts/run-qt5.15 new file mode 100755 index 000000000..e04622eeb --- /dev/null +++ b/scripts/run-qt5.15 @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e + +export PYTHONWARNINGS=default +bazel run $BUILDARGS //qt:runanki_qt515 -- $*