From b5e9eba26f71a65c6e16591007ed00a5288915a8 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 14 Oct 2021 21:48:50 +1000 Subject: [PATCH 1/3] use ResourceReader for serving bundled web files --- qt/aqt/mediasrv.py | 105 ++++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 31 deletions(-) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index a728211ef..3e88c4274 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging +import mimetypes import os import re import sys @@ -30,27 +31,7 @@ from aqt.changenotetype import ChangeNotetypeDialog from aqt.deckoptions import DeckOptionsDialog from aqt.operations.deck import update_deck_configs from aqt.qt import * -from aqt.utils import aqt_data_folder - -def _getExportFolder() -> str: - if not (data_folder := os.getenv("ANKI_DATA_FOLDER")): - data_folder = aqt_data_folder() - webInSrcFolder = os.path.abspath(os.path.join(data_folder, "web")) - if os.path.exists(webInSrcFolder): - return webInSrcFolder - elif isMac: - dir = os.path.dirname(os.path.abspath(__file__)) - return os.path.abspath(f"{dir}/../../Resources/web") - else: - if os.environ.get("TEST_TARGET"): - # running tests in bazel; we have no data - return "." - else: - raise Exception("couldn't find web folder") - - -_exportFolder = _getExportFolder() app = flask.Flask(__name__, root_path="/fake") flask_cors.CORS(app) @@ -63,6 +44,12 @@ class LocalFileRequest: path: str +@dataclass +class BundledFileRequest: + # path relative to aqt data folder + path: str + + @dataclass class NotFound: message: str @@ -135,6 +122,19 @@ class MediaServer(threading.Thread): pass +def _mime_for_path(path: str) -> str: + "Mime type for provided path/filename." + if path.endswith(".css"): + # some users may have invalid mime type in the Windows registry + return "text/css" + elif path.endswith(".js"): + return "application/javascript" + else: + # autodetect + mime, _encoding = mimetypes.guess_type(path) + return mime or "application/octet-stream" + + def _handle_local_file_request(request: LocalFileRequest) -> Response: directory = request.root path = request.path @@ -164,14 +164,7 @@ def _handle_local_file_request(request: LocalFileRequest) -> Response: ) try: - if fullpath.endswith(".css"): - # some users may have invalid mime type in the Windows registry - mimetype = "text/css" - elif fullpath.endswith(".js"): - mimetype = "application/javascript" - else: - # autodetect - mimetype = None + mimetype = _mime_for_path(fullpath) if os.path.exists(fullpath): return flask.send_file(fullpath, mimetype=mimetype, conditional=True) else: @@ -197,6 +190,51 @@ def _handle_local_file_request(request: LocalFileRequest) -> Response: ) +def _builtin_data(path: str) -> bytes: + """Return data from file in aqt/data folder. + Path must use forward slash separators.""" + # overriden location? + if data_folder := os.getenv("ANKI_DATA_FOLDER"): + full_path = os.path.join(data_folder, path) + with open(full_path, "rb") as f: + return f.read() + else: + if isWin and not getattr(sys, "frozen", False) : + # default Python resource loader expects backslashes on Windows + path = path.replace("/", "\\") + reader = aqt.__loader__.get_resource_reader("aqt") # type: ignore + with reader.open_resource(path) as f: + return f.read() + + +def _handle_builtin_file_request(request: BundledFileRequest) -> Response: + path = request.path + mimetype = _mime_for_path(path) + data_path = f"data/web/{path}" + try: + data = _builtin_data(data_path) + return Response(data, mimetype=mimetype) + except FileNotFoundError: + return flask.make_response( + f"Invalid path: {path}", + HTTPStatus.NOT_FOUND, + ) + except Exception as error: + if devMode: + print( + "Caught HTTP server exception,\n%s" + % "".join(traceback.format_exception(*sys.exc_info())), + ) + + # swallow it - user likely surfed away from + # review screen before an image had finished + # downloading + return flask.make_response( + str(error), + HTTPStatus.INTERNAL_SERVER_ERROR, + ) + + @app.route("/", methods=["GET", "POST"]) def handle_request(pathin: str) -> Response: request = _extract_request(pathin) @@ -211,6 +249,8 @@ def handle_request(pathin: str) -> Response: ) elif callable(request): return _handle_dynamic_request(request) + elif isinstance(request, BundledFileRequest): + return _handle_builtin_file_request(request) elif isinstance(request, LocalFileRequest): return _handle_local_file_request(request) else: @@ -222,7 +262,7 @@ def handle_request(pathin: str) -> Response: def _extract_internal_request( path: str, -) -> LocalFileRequest | DynamicRequest | NotFound | None: +) -> BundledFileRequest | DynamicRequest | NotFound | None: "Catch /_anki references and rewrite them to web export folder." prefix = "_anki/" if not path.startswith(prefix): @@ -237,6 +277,7 @@ def _extract_internal_request( return _extract_collection_post_request(filename) elif get_handler := _extract_dynamic_get_request(filename): return get_handler + # remap legacy top-level references base, ext = os.path.splitext(filename) if ext == ".css": @@ -267,7 +308,7 @@ def _extract_internal_request( path = f"{prefix}{additional_prefix}{base}{ext}" print(f"legacy {oldpath} remapped to {path}") - return LocalFileRequest(root=_exportFolder, path=path[len(prefix) :]) + return BundledFileRequest(path=path[len(prefix) :]) def _extract_addon_request(path: str) -> LocalFileRequest | NotFound | None: @@ -302,7 +343,9 @@ def _extract_addon_request(path: str) -> LocalFileRequest | NotFound | None: return NotFound(message=f"couldn't locate item in add-on folder {path}") -def _extract_request(path: str) -> LocalFileRequest | DynamicRequest | NotFound: +def _extract_request( + path: str, +) -> LocalFileRequest | BundledFileRequest | DynamicRequest | NotFound: if internal := _extract_internal_request(path): return internal elif addon := _extract_addon_request(path): From 5a8e064a7d0d6b77c295f67848448e39fdef29c9 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 28 Oct 2021 18:46:45 +1000 Subject: [PATCH 2/3] updated package scripts --- Cargo.toml | 1 + pylib/anki/syncserver/__init__.py | 2 +- qt/aqt/__init__.py | 33 +- qt/aqt/mediasrv.py | 2 +- qt/aqt/package.py | 68 ++++ qt/aqt/platform.py | 6 +- qt/aqt/sound.py | 20 +- qt/aqt/utils.py | 26 +- qt/bazelfixes.py | 2 +- qt/package/.cargo/config | 42 ++ qt/package/Cargo.lock | 644 ++++++++++++++++++++++++++++++ qt/package/Cargo.toml | 52 +++ qt/package/anki-icon.ico | Bin 0 -> 103420 bytes qt/package/anki-manifest.rc | 3 + qt/package/anki.exe.manifest | 8 + qt/package/build.bat | 21 + qt/package/build.py | 226 +++++++++++ qt/package/build.rs | 109 +++++ qt/package/build.sh | 29 ++ qt/package/pyoxidizer.bzl | 162 ++++++++ qt/package/qt.exclude | 9 + qt/package/src/main.rs | 30 ++ qt/tests/run_format.py | 2 + repos.bzl | 2 +- scripts/build.bat | 9 +- scripts/cargo-env | 13 +- scripts/cargo-env.bat | 2 + scripts/copyright_headers.py | 2 + scripts/docker/Dockerfile.amd64 | 2 + scripts/docker/Dockerfile.arm64 | 2 + scripts/docker/build-entrypoint | 8 +- scripts/docker/build.sh | 2 +- scripts/python | 2 +- scripts/python.bat | 1 + 34 files changed, 1482 insertions(+), 60 deletions(-) create mode 100644 qt/aqt/package.py create mode 100644 qt/package/.cargo/config create mode 100644 qt/package/Cargo.lock create mode 100644 qt/package/Cargo.toml create mode 100644 qt/package/anki-icon.ico create mode 100644 qt/package/anki-manifest.rc create mode 100644 qt/package/anki.exe.manifest create mode 100755 qt/package/build.bat create mode 100644 qt/package/build.py create mode 100644 qt/package/build.rs create mode 100755 qt/package/build.sh create mode 100644 qt/package/pyoxidizer.bzl create mode 100644 qt/package/qt.exclude create mode 100644 qt/package/src/main.rs create mode 100644 scripts/cargo-env.bat create mode 100755 scripts/python.bat diff --git a/Cargo.toml b/Cargo.toml index 953c9bd5c..ab23d3802 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ license = "AGPL-3.0-or-later" [workspace] members = ["rslib", "rslib/i18n", "pylib/rsbridge"] +exclude = ["qt/package"] [lib] # dummy top level for tooling diff --git a/pylib/anki/syncserver/__init__.py b/pylib/anki/syncserver/__init__.py index cadee9a8c..72bfd500f 100644 --- a/pylib/anki/syncserver/__init__.py +++ b/pylib/anki/syncserver/__init__.py @@ -31,7 +31,7 @@ from anki.sync_pb2 import SyncServerMethodRequest Method = SyncServerMethodRequest.Method # pylint: disable=no-member -app = flask.Flask(__name__) +app = flask.Flask(__name__, root_path="/fake") col: Collection trace = os.getenv("TRACE") diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index d15d03640..0f3391f1a 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -3,13 +3,27 @@ from __future__ import annotations +import sys + +if sys.version_info[0] < 3 or sys.version_info[1] < 9: + raise Exception("Anki requires Python 3.9+") + +# ensure unicode filenames are supported +try: + "テスト".encode(sys.getfilesystemencoding()) +except UnicodeEncodeError as exc: + raise Exception("Anki requires a UTF-8 locale.") from exc + +from .package import packaged_build_setup + +packaged_build_setup() + import argparse import builtins import cProfile import getpass import locale import os -import sys import tempfile import traceback from typing import Any, Callable, Optional, cast @@ -24,15 +38,6 @@ from aqt import gui_hooks from aqt.qt import * from aqt.utils import TR, tr -if sys.version_info[0] < 3 or sys.version_info[1] < 9: - raise Exception("Anki requires Python 3.9+") - -# ensure unicode filenames are supported -try: - "テスト".encode(sys.getfilesystemencoding()) -except UnicodeEncodeError as exc: - raise Exception("Anki requires a UTF-8 locale.") from exc - # compat aliases anki.version = _version # type: ignore anki.Collection = Collection # type: ignore @@ -233,12 +238,8 @@ def setupLangAndBackend( # load qt translations _qtrans = QTranslator() - from aqt.utils import aqt_data_folder - if isMac and getattr(sys, "frozen", False): - qt_dir = os.path.abspath( - os.path.join(aqt_data_folder(), "..", "qt_translations") - ) + qt_dir = os.path.join(sys.prefix, "../Resources/qt_translations") else: if qtmajor == 5: qt_dir = QLibraryInfo.location(QLibraryInfo.TranslationsPath) # type: ignore @@ -429,6 +430,7 @@ def write_profile_results() -> None: def run() -> None: + print("Preparing to run...") try: _run() except Exception as e: @@ -617,6 +619,7 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp mw = aqt.main.AnkiQt(app, pm, backend, opts, args) if exec: + print("Starting main loop...") app.exec() else: return app diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 3e88c4274..948c418c8 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -199,7 +199,7 @@ def _builtin_data(path: str) -> bytes: with open(full_path, "rb") as f: return f.read() else: - if isWin and not getattr(sys, "frozen", False) : + if isWin and not getattr(sys, "frozen", False): # default Python resource loader expects backslashes on Windows path = path.replace("/", "\\") reader = aqt.__loader__.get_resource_reader("aqt") # type: ignore diff --git a/qt/aqt/package.py b/qt/aqt/package.py new file mode 100644 index 000000000..65a30968a --- /dev/null +++ b/qt/aqt/package.py @@ -0,0 +1,68 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +"""Helpers for the packaged version of Anki.""" + +from __future__ import annotations + +import os +import sys + + +def _fix_pywin32() -> None: + # extend sys.path with .pth files + import site + + site.addsitedir(sys.path[0]) + + # use updated sys.path to locate dll folder and add it to path + path = sys.path[-1] + path = path.replace("Pythonwin", "pywin32_system32") + os.environ["PATH"] += ";" + path + + # import Python modules from .dll files + import importlib.machinery + + for name in "pythoncom", "pywintypes": + filename = os.path.join(path, name + "39.dll") + loader = importlib.machinery.ExtensionFileLoader(name, filename) + spec = importlib.machinery.ModuleSpec(name=name, loader=loader, origin=filename) + _mod = importlib._bootstrap._load(spec) # type: ignore + + +def _patch_pkgutil() -> None: + """Teach pkgutil.get_data() how to read files from in-memory resources. + + This is required for jsonschema.""" + import importlib + import pkgutil + + def get_data_custom(package: str, resource: str) -> bytes | None: + try: + module = importlib.import_module(package) + reader = module.__loader__.get_resource_reader(package) # type: ignore[attr-defined] + with reader.open_resource(resource) as f: + return f.read() + except: + return None + + pkgutil.get_data = get_data_custom + + +def packaged_build_setup() -> None: + if not getattr(sys, "frozen", False): + return + + print("Initial setup...") + + if sys.platform == "win32": + _fix_pywin32() + + _patch_pkgutil() + + # escape hatch for debugging issues with packaged build startup + if os.getenv("ANKI_STARTUP_REPL") and os.isatty(sys.stdin.fileno()): + import code + + code.InteractiveConsole().interact() + sys.exit(0) diff --git a/qt/aqt/platform.py b/qt/aqt/platform.py index 4354eade2..fdf038e1b 100644 --- a/qt/aqt/platform.py +++ b/qt/aqt/platform.py @@ -4,6 +4,7 @@ """Platform-specific functionality.""" import os +import sys from ctypes import CDLL import aqt.utils @@ -24,5 +25,8 @@ def set_dark_mode(enabled: bool) -> bool: def _set_dark_mode(enabled: bool) -> None: - path = os.path.join(aqt.utils.aqt_data_folder(), "lib", "libankihelper.dylib") + if getattr(sys, "frozen", False): + path = os.path.join(sys.prefix, "libankihelper.dylib") + else: + path = os.path.join(aqt.utils.aqt_data_folder(), "lib", "libankihelper.dylib") CDLL(path).set_darkmode_enabled(enabled) diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index ee6c38ae2..880af90ca 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -14,6 +14,7 @@ import wave from abc import ABC, abstractmethod from concurrent.futures import Future from operator import itemgetter +from pathlib import Path from typing import Any, Callable, cast from markdown import markdown @@ -234,16 +235,15 @@ def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]: if "LD_LIBRARY_PATH" in env: del env["LD_LIBRARY_PATH"] if isMac: - dir = os.path.dirname(os.path.abspath(__file__)) - exeDir = os.path.abspath(f"{dir}/../../Resources/audio") - else: - exeDir = os.path.dirname(os.path.abspath(sys.argv[0])) - if isWin and not cmd[0].endswith(".exe"): - cmd[0] += ".exe" - path = os.path.join(exeDir, cmd[0]) - if not os.path.exists(path): - return cmd, env - cmd[0] = path + path = Path(sys.prefix).joinpath("audio").joinpath(cmd[0]) + if path.exists(): + cmd[0] = str(path) + return cmd, env + adjusted_path = os.path.join(sys.prefix, cmd[0]) + if isWin and not adjusted_path.endswith(".exe"): + adjusted_path += ".exe" + if os.path.exists(adjusted_path): + cmd[0] = adjusted_path return cmd, env diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index ed5699059..f0ff5e49f 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -30,21 +30,19 @@ def aqt_data_folder() -> str: # running in Bazel on macOS? if path := os.getenv("AQT_DATA_FOLDER"): return path - # running in place? - dir = os.path.join(os.path.dirname(__file__), "data") - if os.path.exists(dir): - return dir - # packaged install? - if isMac: - dir2 = os.path.join(sys.prefix, "..", "Resources", "aqt_data") + # packaged? + elif getattr(sys, "frozen", False): + path = os.path.join(sys.prefix, "lib/aqt/data") + if os.path.exists(path): + return path + else: + return os.path.join(sys.prefix, "../Resources/aqt/data") + elif os.path.exists(dir := os.path.join(os.path.dirname(__file__), "data")): + return os.path.abspath(dir) else: - dir2 = os.path.join(sys.prefix, "aqt_data") - if os.path.exists(dir2): - return dir2 - - # should only happen when running unit tests - print("warning, data folder not found") - return "." + # should only happen when running unit tests + print("warning, data folder not found") + return "." # shortcut to access Fluent translations; set as diff --git a/qt/bazelfixes.py b/qt/bazelfixes.py index 768299443..b3f1eb594 100644 --- a/qt/bazelfixes.py +++ b/qt/bazelfixes.py @@ -33,7 +33,7 @@ def fix_pywin32_in_bazel(force=False): import importlib.machinery name = "pythoncom" - filename = os.path.join(path, "pythoncom38.dll") + filename = os.path.join(path, "pythoncom39.dll") loader = importlib.machinery.ExtensionFileLoader(name, filename) spec = importlib.machinery.ModuleSpec(name=name, loader=loader, origin=filename) _mod = importlib._bootstrap._load(spec) diff --git a/qt/package/.cargo/config b/qt/package/.cargo/config new file mode 100644 index 000000000..0a543e7c1 --- /dev/null +++ b/qt/package/.cargo/config @@ -0,0 +1,42 @@ +# By default Rust will not export dynamic symbols from built executables. +# Python symbols need to be exported from executables in order for that +# executable to load Python extension modules, which are shared libraries. +# Otherwise, the extension module / shared library is unable to resolve +# Python symbols. This file contains target-specific configuration +# overrides to export dynamic symbols from executables. +# +# Ideally we would achieve this functionality via the build.rs build +# script. But custom compiler flags via build scripts apparently only +# support limited options. + +[target.i686-unknown-linux-gnu] +rustflags = ["-C", "link-args=-Wl,-export-dynamic"] + +[target.x86_64-unknown-linux-gnu] +rustflags = ["-C", "link-args=-Wl,-export-dynamic"] + +[target.aarch64-apple-darwin] +rustflags = ["-C", "link-args=-rdynamic"] + +[target.x86_64-apple-darwin] +rustflags = ["-C", "link-args=-rdynamic"] + +# The Windows standalone_static distributions use the static CRT (/MT compiler +# flag). By default, Rust will build with the dynamically linked / DLL CRT +# (/MD compiler flag). `pyoxidizer build` should adjust RUSTFLAGS automatically +# when a standalone_static distribution is being used. But if invoking `cargo` +# directly, you'll need to override the default CRT linkage by either passing +# RUSTFLAGS="-C target-feature=+crt-static" or by commenting out the lines +# below. Note that use of `target-feature=+crt-static` will prevent +# standalone_dynamic distributions from working. +# +# The standalone_static distributions also have duplicate symbols and some +# build configurations will result in hard linker errors because of this. We +# also add the /FORCE:MULTIPLE linker argument to prevent this from being a +# fatal error. + +#[target.i686-pc-windows-msvc] +#rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/FORCE:MULTIPLE"] +# +#[target.x86_64-pc-windows-msvc] +#rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/FORCE:MULTIPLE"] diff --git a/qt/package/Cargo.lock b/qt/package/Cargo.lock new file mode 100644 index 000000000..a1518ebe7 --- /dev/null +++ b/qt/package/Cargo.lock @@ -0,0 +1,644 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "anki" +version = "0.1.0" +dependencies = [ + "embed-resource", + "jemallocator", + "mimalloc", + "pyembed", + "snmalloc-rs", +] + +[[package]] +name = "anyhow" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" + +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "charset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f426e64df1c3de26cbf44593c6ffff5dbfd43bbf9de0d075058558126b3fc73" +dependencies = [ + "base64 0.10.1", + "encoding_rs", +] + +[[package]] +name = "cmake" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b858541263efe664aead4a5209a4ae5c5d2811167d4ed4ee0944503f8d2089" +dependencies = [ + "cc", +] + +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + +[[package]] +name = "dunce" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "embed-resource" +version = "1.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85505eb239fc952b300f29f0556d2d884082a83566768d980278d8faf38c780d" +dependencies = [ + "cc", + "vswhom", + "winreg", +] + +[[package]] +name = "encoding_rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + +[[package]] +name = "indoc" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8" +dependencies = [ + "indoc-impl", + "proc-macro-hack", +] + +[[package]] +name = "indoc-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", + "unindent", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "jemalloc-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d3b9f3f5c9b31aa0f5ed3260385ac205db665baa41d49bb8338008ae94ede45" +dependencies = [ + "cc", + "fs_extra", + "libc", +] + +[[package]] +name = "jemallocator" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ae63fcfc45e99ab3d1b29a46782ad679e98436c3169d15a167a1108a724b69" +dependencies = [ + "jemalloc-sys", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" + +[[package]] +name = "libmimalloc-sys" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1b8479c593dba88c2741fc50b92e13dbabbbe0bd504d979f244ccc1a5b1c01" +dependencies = [ + "cc", + "cty", +] + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "mailparse" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee6e1ca1c8396da58f8128176f6980dd57bec84c8670a479519d3655f2d6734" +dependencies = [ + "base64 0.13.0", + "charset", + "quoted_printable", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memmap" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "memory-module-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bbdce2925c681860b08875119254fb5543dbf6337c56ff93afebeed9c686da3" +dependencies = [ + "cc", + "libc", + "winapi", +] + +[[package]] +name = "mimalloc" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb74897ce508e6c49156fd1476fc5922cbc6e75183c65e399c765a09122e5130" +dependencies = [ + "libmimalloc-sys", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "paste" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "pyembed" +version = "0.18.0-pre" +source = "git+https://github.com/ankitects/PyOxidizer.git?rev=ffbfe66912335bc816074c7a08aed06e26bfca7f#ffbfe66912335bc816074c7a08aed06e26bfca7f" +dependencies = [ + "anyhow", + "dunce", + "jemalloc-sys", + "libmimalloc-sys", + "once_cell", + "pyo3", + "pyo3-build-config", + "python-oxidized-importer", + "python-packaging", + "snmalloc-sys", +] + +[[package]] +name = "pyo3" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35100f9347670a566a67aa623369293703322bb9db77d99d7df7313b575ae0c8" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "parking_lot", + "paste", + "pyo3-build-config", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d12961738cacbd7f91b7c43bc25cfeeaa2698ad07a04b3be0aa88b950865738f" +dependencies = [ + "once_cell", +] + +[[package]] +name = "pyo3-macros" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0bc5215d704824dfddddc03f93cb572e1155c68b6761c37005e1c288808ea8" +dependencies = [ + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71623fc593224afaab918aa3afcaf86ed2f43d34f6afde7f3922608f253240df" +dependencies = [ + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "python-oxidized-importer" +version = "0.3.0-pre" +source = "git+https://github.com/ankitects/PyOxidizer.git?rev=ffbfe66912335bc816074c7a08aed06e26bfca7f#ffbfe66912335bc816074c7a08aed06e26bfca7f" +dependencies = [ + "anyhow", + "lazy_static", + "memmap", + "memory-module-sys", + "once_cell", + "pyo3", + "python-packaging", + "python-packed-resources", + "tugger-file-manifest", + "winapi", +] + +[[package]] +name = "python-packaging" +version = "0.11.0-pre" +source = "git+https://github.com/ankitects/PyOxidizer.git?rev=ffbfe66912335bc816074c7a08aed06e26bfca7f#ffbfe66912335bc816074c7a08aed06e26bfca7f" +dependencies = [ + "anyhow", + "byteorder", + "encoding_rs", + "itertools", + "mailparse", + "once_cell", + "python-packed-resources", + "regex", + "spdx", + "tugger-file-manifest", + "tugger-licensing", + "walkdir", +] + +[[package]] +name = "python-packed-resources" +version = "0.8.0-pre" +source = "git+https://github.com/ankitects/PyOxidizer.git?rev=ffbfe66912335bc816074c7a08aed06e26bfca7f#ffbfe66912335bc816074c7a08aed06e26bfca7f" +dependencies = [ + "anyhow", + "byteorder", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1238256b09923649ec89b08104c4dfe9f6cb2fea734a5db5384e44916d59e9c5" + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + +[[package]] +name = "snmalloc-rs" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36acaace2719c972eab3ef6a6b3aee4495f0bf300f59715bb9cff6c5acf4ae20" +dependencies = [ + "snmalloc-sys", +] + +[[package]] +name = "snmalloc-sys" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a7e6e7d5fe756bee058ddedefc7e0a9f9c8dbaa9401b48ed3c17d6578e40b5" +dependencies = [ + "cc", + "cmake", +] + +[[package]] +name = "spdx" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e1bff9c842210e48eb85ce4c24983b34a481af4ba4b6140b41737e432f4030b" +dependencies = [ + "smallvec", +] + +[[package]] +name = "syn" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tugger-file-manifest" +version = "0.6.0-pre" +source = "git+https://github.com/ankitects/PyOxidizer.git?rev=ffbfe66912335bc816074c7a08aed06e26bfca7f#ffbfe66912335bc816074c7a08aed06e26bfca7f" + +[[package]] +name = "tugger-licensing" +version = "0.5.0-pre" +source = "git+https://github.com/ankitects/PyOxidizer.git?rev=ffbfe66912335bc816074c7a08aed06e26bfca7f#ffbfe66912335bc816074c7a08aed06e26bfca7f" +dependencies = [ + "anyhow", + "spdx", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "unindent" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f5402d3d0e79a069714f7b48e3ecc60be7775a2c049cb839457457a239532" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] diff --git a/qt/package/Cargo.toml b/qt/package/Cargo.toml new file mode 100644 index 000000000..2403cc008 --- /dev/null +++ b/qt/package/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "anki" +version = "0.1.0" +build = "build.rs" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies.pyembed] +git = "https://github.com/ankitects/PyOxidizer.git" +rev = "ffbfe66912335bc816074c7a08aed06e26bfca7f" +default-features = false + +[dependencies.jemallocator] +version = "0.3" +optional = true + +[dependencies.mimalloc] +version = "0.1" +optional = true +features = ["local_dynamic_tls", "override", "secure"] + +[dependencies.snmalloc-rs] +version = "0.2" +optional = true + +[build-dependencies] +embed-resource = "1.6" + +[features] +default = ["build-mode-standalone"] + +global-allocator-jemalloc = ["jemallocator"] +global-allocator-mimalloc = ["mimalloc"] +global-allocator-snmalloc = ["snmalloc-rs"] + +allocator-jemalloc = ["pyembed/allocator-jemalloc"] +allocator-mimalloc = ["pyembed/allocator-mimalloc"] +allocator-snmalloc = ["pyembed/allocator-snmalloc"] + +# Build this crate in isolation, without using PyOxidizer. +build-mode-standalone = [] + +# Build this crate by executing a `pyoxidizer` executable to build +# required artifacts. +build-mode-pyoxidizer-exe = [] + +# Build this crate by reusing artifacts generated by `pyoxidizer` out-of-band. +# In this mode, the PYOXIDIZER_ARTIFACT_DIR environment variable can refer +# to the directory containing build artifacts produced by `pyoxidizer`. If not +# set, OUT_DIR will be used. +build-mode-prebuilt-artifacts = [] diff --git a/qt/package/anki-icon.ico b/qt/package/anki-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fd03c333ea8f5f8b0d057e3541dad7e4d7ef66dc GIT binary patch literal 103420 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 + + + + true + + + diff --git a/qt/package/build.bat b/qt/package/build.bat new file mode 100755 index 000000000..4320c8053 --- /dev/null +++ b/qt/package/build.bat @@ -0,0 +1,21 @@ +:: ensure wheels are built +pushd ..\.. +call scripts\build || exit /b +set ROOT=%CD% +popd + +:: ensure venv exists +set OUTPUT_ROOT=%ROOT%/bazel-pkg +set VENV=%OUTPUT_ROOT%/venv +if not exist %VENV% ( + mkdir %OUTPUT_ROOT% + pushd %ROOT% + call scripts\python -m venv %VENV% || exit /b + popd +) + +:: run the rest of the build in Python +FOR /F "tokens=*" %%g IN ('call ..\..\bazel.bat info output_base --ui_event_filters=-INFO') do (SET BAZEL_EXTERNAL=%%g/external) +call %ROOT%\scripts\cargo-env +call ..\..\bazel.bat query @pyqt515//:* +%VENV%\scripts\python build.py %ROOT% %BAZEL_EXTERNAL% || exit /b diff --git a/qt/package/build.py b/qt/package/build.py new file mode 100644 index 000000000..546d5dd33 --- /dev/null +++ b/qt/package/build.py @@ -0,0 +1,226 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import glob +import os +import platform +import re +import shutil +import subprocess +import sys +from pathlib import Path + +is_win = sys.platform == "win32" + +workspace = Path(sys.argv[1]) +output_root = workspace / "bazel-pkg" +dist_folder = output_root / "dist" +venv = output_root / "venv" +cargo_target = output_root / "target" +bazel_external = Path(sys.argv[2]) +artifacts = output_root / "artifacts" +pyo3_config = output_root / "pyo3-build-config-file.txt" + +if is_win: + python_bin_folder = venv / "scripts" + os.environ["PATH"] = os.getenv("USERPROFILE") + r"\.cargo\bin;" + os.getenv("PATH") + cargo_features = "build-mode-prebuilt-artifacts" +else: + python_bin_folder = venv / "bin" + os.environ["PATH"] = os.getenv("HOME") + "/.cargo/bin:" + os.getenv("PATH") + cargo_features = ( + "build-mode-prebuilt-artifacts global-allocator-jemalloc allocator-jemalloc" + ) + +os.environ["PYOXIDIZER_ARTIFACT_DIR"] = str(artifacts) +os.environ["PYOXIDIZER_CONFIG"] = str(Path(os.getcwd()) / "pyoxidizer.bzl") +os.environ["CARGO_TARGET_DIR"] = str(cargo_target) + +# OS-specific things +pyqt5_folder_name = "pyqt515" +if is_win: + os.environ["TARGET"] = "x86_64-pc-windows-msvc" +elif sys.platform.startswith("darwin"): + if platform.machine() == "arm64": + pyqt5_folder_name = None + os.environ["TARGET"] = "aarch64-apple-darwin" + os.environ["MACOSX_DEPLOYMENT_TARGET"] = "11.0" + else: + pyqt5_folder_name = "pyqt514" + os.environ["TARGET"] = "x86_64-apple-darwin" + os.environ["MACOSX_DEPLOYMENT_TARGET"] = "10.13" +else: + if platform.machine() == "x86_64": + os.environ["TARGET"] = "x86_64-unknown-linux-gnu" + else: + os.environ["TARGET"] = "aarch64-unknown-linux-gnu" + raise Exception("building on this architecture is not currently supported") + + +python = python_bin_folder / "python" +pip = python_bin_folder / "pip" +artifacts_in_build = ( + output_root + / "build" + / os.getenv("TARGET") + / "release" + / "resources" + / "extra_files" +) + + +def build_pyoxidizer(): + subprocess.run( + [ + "cargo", + "install", + "--locked", + "--git", + "https://github.com/ankitects/PyOxidizer.git", + "--rev", + "ffbfe66912335bc816074c7a08aed06e26bfca7f", + "pyoxidizer", + ], + check=True, + ) + + +def install_wheels_into_venv(): + # Pip's handling of hashes is somewhat broken. It spots the hashes in the constraints + # file and forces all files to have a hash. We can manually hash our generated wheels + # and pass them in with hashes, but it still breaks, because the 'protobuf>=3.17' + # specifier in the pylib wheel is not allowed. Nevermind that a specific version is + # included in the constraints file we pass along! To get things working, we're + # forced to strip the hashes out before installing. This should be safe, as the files + # have already been validated as part of the build process. + constraints = output_root / "deps_without_hashes.txt" + with open(workspace / "python" / "requirements.txt") as f: + buf = f.read() + with open(constraints, "w") as f: + extracted = re.findall("^(\S+==\S+) ", buf, flags=re.M) + f.write("\n".join(extracted)) + + # install wheels and upgrade any deps + wheels = glob.glob(str(workspace / "bazel-dist" / "*.whl")) + subprocess.run( + [pip, "install", "--upgrade", "-c", constraints, *wheels], check=True + ) + # always reinstall our wheels + subprocess.run( + [pip, "install", "--force-reinstall", "--no-deps", *wheels], check=True + ) + + +def build_artifacts(): + if os.path.exists(artifacts): + shutil.rmtree(artifacts) + if os.path.exists(artifacts_in_build): + shutil.rmtree(artifacts_in_build) + + subprocess.run( + [ + "pyoxidizer", + "--system-rust", + "run-build-script", + "build.rs", + "--var", + "venv", + venv, + ], + check=True, + env=os.environ + | dict( + CARGO_MANIFEST_DIR=".", + OUT_DIR=str(artifacts), + PROFILE="release", + PYO3_PYTHON=str(python), + ), + ) + + existing_config = None + if os.path.exists(pyo3_config): + with open(pyo3_config) as f: + existing_config = f.read() + + with open(artifacts / "pyo3-build-config-file.txt") as f: + new_config = f.read() + + # avoid bumping mtime, which triggers crate recompile + if new_config != existing_config: + with open(pyo3_config, "w") as f: + f.write(new_config) + + +def build_pkg(): + subprocess.run( + [ + "cargo", + "build", + "--release", + "--no-default-features", + "--features", + cargo_features, + ], + check=True, + env=os.environ | dict(PYO3_CONFIG_FILE=str(pyo3_config)), + ) + + +def adj_path_for_windows_rsync(path: Path) -> str: + if not is_win: + return str(path) + + path = path.absolute() + rest = str(path)[2:].replace("\\", "/") + return f"/{path.drive[0]}{rest}" + + +def merge_into_dist(output_folder: Path, pyqt_src_path: Path): + if not output_folder.exists(): + output_folder.mkdir(parents=True) + # PyQt + subprocess.run( + [ + "rsync", + "-a", + "--delete", + "--exclude-from", + "qt.exclude", + adj_path_for_windows_rsync(pyqt_src_path), + adj_path_for_windows_rsync(output_folder / "lib") + "/", + ], + check=True, + ) + # Executable and other resources + resources = [ + adj_path_for_windows_rsync( + cargo_target / "release" / ("anki.exe" if is_win else "anki") + ), + adj_path_for_windows_rsync(artifacts_in_build) + "/", + ] + if is_win: + resources.append(adj_path_for_windows_rsync(Path("win")) + "/") + + subprocess.run( + [ + "rsync", + "-a", + "--delete", + "--exclude", + "PyQt6", + "--exclude", + "PyQt5", + *resources, + adj_path_for_windows_rsync(output_folder) + "/", + ], + check=True, + ) + + +build_pyoxidizer() +install_wheels_into_venv() +build_artifacts() +build_pkg() +merge_into_dist(dist_folder / "std", bazel_external / "pyqt6" / "PyQt6") +if pyqt5_folder_name: + merge_into_dist(dist_folder / "alt", bazel_external / pyqt5_folder_name / "PyQt5") diff --git a/qt/package/build.rs b/qt/package/build.rs new file mode 100644 index 000000000..75a6d64c4 --- /dev/null +++ b/qt/package/build.rs @@ -0,0 +1,109 @@ +// Based off PyOxidizer's 'init-rust-project'. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use { + embed_resource, + std::path::{Path, PathBuf}, +}; + +const DEFAULT_PYTHON_CONFIG_FILENAME: &str = "default_python_config.rs"; +const DEFAULT_PYTHON_CONFIG: &str = "\ +pub fn default_python_config<'a>() -> pyembed::OxidizedPythonInterpreterConfig<'a> { + pyembed::OxidizedPythonInterpreterConfig::default() +} +"; + +/// Build with PyOxidizer artifacts in a directory. +fn build_with_artifacts_in_dir(path: &Path) { + println!("using pre-built artifacts from {}", path.display()); + let config_path = path.join(DEFAULT_PYTHON_CONFIG_FILENAME); + if !config_path.exists() { + panic!( + "{} does not exist; is {} a valid artifacts directory?", + config_path.display(), + path.display() + ); + } + println!( + "cargo:rustc-env=DEFAULT_PYTHON_CONFIG_RS={}", + config_path.display() + ); +} + +/// Build by calling a `pyoxidizer` executable to generate build artifacts. +fn build_with_pyoxidizer_exe(exe: Option, resolve_target: Option<&str>) { + let pyoxidizer_exe = if let Some(path) = exe { + path + } else { + "pyoxidizer".to_string() + }; + + let mut args = vec!["run-build-script", "build.rs"]; + if let Some(target) = resolve_target { + args.push("--target"); + args.push(target); + } + + match std::process::Command::new(pyoxidizer_exe) + .args(args) + .status() + { + Ok(status) => { + if !status.success() { + panic!("`pyoxidizer run-build-script` failed"); + } + } + Err(e) => panic!("`pyoxidizer run-build-script` failed: {}", e.to_string()), + } +} + +#[allow(clippy::if_same_then_else)] +fn main() { + if std::env::var("CARGO_FEATURE_BUILD_MODE_STANDALONE").is_ok() { + let path = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not defined")); + let path = path.join(DEFAULT_PYTHON_CONFIG_FILENAME); + + std::fs::write(&path, DEFAULT_PYTHON_CONFIG.as_bytes()) + .expect("failed to write default python config"); + println!( + "cargo:rustc-env=DEFAULT_PYTHON_CONFIG_RS={}", + path.display() + ); + } else if std::env::var("CARGO_FEATURE_BUILD_MODE_PYOXIDIZER_EXE").is_ok() { + let target = if let Ok(target) = std::env::var("PYOXIDIZER_BUILD_TARGET") { + Some(target) + } else { + None + }; + + build_with_pyoxidizer_exe( + std::env::var("PYOXIDIZER_EXE").ok(), + target.as_ref().map(|target| target.as_ref()), + ); + } else if std::env::var("CARGO_FEATURE_BUILD_MODE_PREBUILT_ARTIFACTS").is_ok() { + let artifact_dir_env = std::env::var("PYOXIDIZER_ARTIFACT_DIR"); + + let artifact_dir_path = match artifact_dir_env { + Ok(ref v) => PathBuf::from(v), + Err(_) => { + let out_dir = std::env::var("OUT_DIR").unwrap(); + PathBuf::from(&out_dir) + } + }; + + println!("cargo:rerun-if-env-changed=PYOXIDIZER_ARTIFACT_DIR"); + build_with_artifacts_in_dir(&artifact_dir_path); + } else { + panic!("build-mode-* feature not set"); + } + + let target_family = + std::env::var("CARGO_CFG_TARGET_FAMILY").expect("CARGO_CFG_TARGET_FAMILY not defined"); + + // embed manifest and icon + if target_family == "windows" { + embed_resource::compile("anki-manifest.rc"); + } +} diff --git a/qt/package/build.sh b/qt/package/build.sh new file mode 100755 index 000000000..bd0d64ed6 --- /dev/null +++ b/qt/package/build.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -e + +cd $(dirname $0) +ROOT=$(pwd)/../.. +OUTPUT_ROOT=$ROOT/bazel-pkg +VENV=$OUTPUT_ROOT/venv +BAZEL_EXTERNAL=$(bazel info output_base --ui_event_filters=-INFO)/external + +# ensure the wheels are built +(cd $ROOT && ./scripts/build) + +# ensure venv exists +test -d $VENV || ( + mkdir -p $OUTPUT_ROOT + (cd $ROOT && ./scripts/python -m venv $VENV) +) + +# run the rest of the build in Python +. $ROOT/scripts/cargo-env +if [[ "$OSTYPE" == "darwin"* ]]; then + if [ $(uname -m) != "arm64" ]; then + bazel query @pyqt514//:* > /dev/null + fi +else + bazel query @pyqt515//:* > /dev/null +fi +$VENV/bin/python build.py $ROOT $BAZEL_EXTERNAL diff --git a/qt/package/pyoxidizer.bzl b/qt/package/pyoxidizer.bzl new file mode 100644 index 000000000..99c2443e3 --- /dev/null +++ b/qt/package/pyoxidizer.bzl @@ -0,0 +1,162 @@ +set_build_path("../../bazel-pkg/build") + +excluded_source_prefixes = [ + "ctypes.test", + "distutils.tests", + "idlelib", + "lib2to3.tests", + "test", + "tkinter", + "win32comext", + "win32com", + "win32", + "pythonwin", +] + +excluded_resource_suffixes = [ + ".pyi", + ".pyc", + "py.typed", +] + +included_resource_packages = [ + "anki", + "aqt", + "lib2to3", + "certifi", + "jsonschema", +] + +def handle_resource(policy, resource): + if type(resource) == "PythonModuleSource": + resource.add_include = True + for prefix in excluded_source_prefixes: + if resource.name.startswith(prefix): + resource.add_include = False + + # if resource.add_include: + # print("src", resource.name, resource.add_include) + + elif type(resource) == "PythonExtensionModule": + resource.add_include = True + if resource.name.startswith("win32"): + resource.add_include = False + + #print("ext", resource.name, resource.add_include) + + elif type(resource) == "PythonPackageResource": + for prefix in included_resource_packages: + if resource.package.startswith(prefix): + resource.add_include = True + for suffix in excluded_resource_suffixes: + if resource.name.endswith(suffix): + resource.add_include = False + + # aqt web resources can be stored in binary + if resource.package == "aqt": + if not resource.name.startswith("data/web"): + resource.add_location = "filesystem-relative:lib" + + # if resource.add_include: + # print("rsrc", resource.package, resource.name, resource.add_include) + + elif type(resource) == "PythonPackageDistributionResource": + #print("dist", resource.package, resource.name, resource.add_include) + pass + + # elif type(resource) == "File": + # print(resource.path) + + elif type(resource) == "File": + if ( + resource.path.startswith("win32") or + resource.path.startswith("pythonwin") or + resource.path.startswith("pywin32") + ): + exclude = ( + "tests" in resource.path or + "benchmark" in resource.path or + "__pycache__" in resource.path + ) + if not exclude: + resource.add_include = True + resource.add_location = "filesystem-relative:lib" + + if ".dist-info" in resource.path: + resource.add_include = False + + else: + print("unexpected type", type(resource)) + +def make_exe(): + if BUILD_TARGET_TRIPLE == "aarch64-unknown-linux-gnu": + fail("arm64 is not currently supported") + elif BUILD_TARGET_TRIPLE == "x86_64-unknown-linux-gnu": + dist = PythonDistribution( + url = "https://github.com/ankitects/python-build-standalone/releases/download/anki-2021-10-15/cpython-3.9.7-x86_64-unknown-linux-gnu-pgo-20211013T1538.tar.zst", + sha256 = "e5341c8f0fbedf83a2246cd86d60b6598033599ae20602d2f80617a304ef3085", + ) + + else: + dist = default_python_distribution() + + policy = dist.make_python_packaging_policy() + + policy.file_scanner_classify_files = True + policy.include_classified_resources = False + + policy.allow_files = True + policy.file_scanner_emit_files = True + policy.include_file_resources = False + + policy.include_distribution_sources = False + policy.include_distribution_resources = False + policy.include_non_distribution_sources = False + policy.include_test = False + + policy.resources_location = "in-memory" + policy.resources_location_fallback = "filesystem-relative:lib" + + policy.register_resource_callback(handle_resource) + + policy.bytecode_optimize_level_zero = False + policy.bytecode_optimize_level_two = True + + python_config = dist.make_python_interpreter_config() + + # detected libs do not need this, but we add extra afterwards + python_config.module_search_paths = ["$ORIGIN/lib"] + python_config.optimization_level = 2 + + python_config.run_command = "import aqt; aqt.run()" + + exe = dist.to_python_executable( + name = "anki", + packaging_policy = policy, + config = python_config, + ) + + exe.windows_runtime_dlls_mode = "always" + + # set in main.rs + exe.windows_subsystem = "console" + + venv_path = "venv" + + resources = exe.read_virtualenv(VARS.get("venv")) + exe.add_python_resources(resources) + + return exe + +def make_embedded_resources(exe): + return exe.to_embedded_resources() + +def make_install(exe): + files = FileManifest() + files.add_python_resource(".", exe) + return files + +register_target("exe", make_exe) +register_target("resources", make_embedded_resources, depends = ["exe"], default_build_script = True) +register_target("install", make_install, depends = ["exe"], default = True) +resolve_targets() diff --git a/qt/package/qt.exclude b/qt/package/qt.exclude new file mode 100644 index 000000000..5c94c3ea9 --- /dev/null +++ b/qt/package/qt.exclude @@ -0,0 +1,9 @@ +qml +bindings +uic +lupdate +qsci +*.pyc +*.pyi +*.sip +py.typed diff --git a/qt/package/src/main.rs b/qt/package/src/main.rs new file mode 100644 index 000000000..6b4084e8a --- /dev/null +++ b/qt/package/src/main.rs @@ -0,0 +1,30 @@ +// Based off PyOxidizer's 'init-rust-project'. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![windows_subsystem = "console"] + +use pyembed::{MainPythonInterpreter, OxidizedPythonInterpreterConfig}; + +#[cfg(feature = "global-allocator-jemalloc")] +#[global_allocator] +static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; + +include!(env!("DEFAULT_PYTHON_CONFIG_RS")); + +fn main() { + println!("Anki starting..."); + + let exit_code = { + let config: OxidizedPythonInterpreterConfig = default_python_config(); + match MainPythonInterpreter::new(config) { + Ok(interp) => interp.run(), + Err(msg) => { + eprintln!("error instantiating embedded Python interpreter: {}", msg); + 1 + } + } + }; + std::process::exit(exit_code); +} diff --git a/qt/tests/run_format.py b/qt/tests/run_format.py index 20e151bd1..a5039ad50 100644 --- a/qt/tests/run_format.py +++ b/qt/tests/run_format.py @@ -33,6 +33,7 @@ if __name__ == "__main__": "aqt", "tests", "tools", + "package", ] + args, check=False, @@ -50,6 +51,7 @@ if __name__ == "__main__": "aqt", "tests", "tools", + "package", ] + args, check=False, diff --git a/repos.bzl b/repos.bzl index b4ba5c8ab..a223cf79b 100644 --- a/repos.bzl +++ b/repos.bzl @@ -2,7 +2,7 @@ Dependencies required to build Anki. """ -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository", "new_git_repository") load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") diff --git a/scripts/build.bat b/scripts/build.bat index 0d55f4d8e..0aa80294f 100755 --- a/scripts/build.bat +++ b/scripts/build.bat @@ -7,9 +7,6 @@ if not exist WORKSPACE ( rd /s /q bazel-dist -set BUILDARGS=-k -c opt dist --color=yes --@rules_rust//worker:use_worker=False -call .\bazel build %BUILDARGS% -:: repeat on failure -IF %ERRORLEVEL% NEQ 0 call .\bazel build %BUILDARGS% - -tar xvf bazel-bin\dist.tar +set BUILDARGS=-k -c opt dist --color=yes +call .\bazel build %BUILDARGS% || exit /b 1 +tar xvf bazel-bin\dist.tar || exit /b 1 diff --git a/scripts/cargo-env b/scripts/cargo-env index d6f673b64..5150155fe 100755 --- a/scripts/cargo-env +++ b/scripts/cargo-env @@ -1,14 +1,13 @@ #!/bin/bash -# Put our vendored version of cargo on the path. Not used by our -# build scripts, but can be helpful if you need quick access to cargo -# on a machine that does not have Rust installed separately, or -# want to run a quick check. Eg: +# Put our vendored version of cargo on the path. Can be helpful if you need +# quick access to cargo on a machine that does not have Rust installed +# separately, or want to run a quick check. Eg: # $ . scripts/cargo-env # $ (cd rslib && cargo check) -BAZEL_EXTERNAL=$(bazel info output_base)/external +BAZEL_EXTERNAL=$(bazel info output_base --ui_event_filters=-INFO)/external if [[ "$OSTYPE" == "darwin"* ]]; then if [ "$(arch)" == "i386" ]; then @@ -17,5 +16,9 @@ if [[ "$OSTYPE" == "darwin"* ]]; then export PATH="$BAZEL_EXTERNAL/rust_darwin_arm64/bin:$PATH" fi else + if [ "$(arch)" == "aarch64" ]; then + export PATH="$BAZEL_EXTERNAL/rust_linux_aarch64/bin:$PATH" + else export PATH="$BAZEL_EXTERNAL/rust_linux_x86_64/bin:$PATH" + fi fi diff --git a/scripts/cargo-env.bat b/scripts/cargo-env.bat new file mode 100644 index 000000000..51f6b2c06 --- /dev/null +++ b/scripts/cargo-env.bat @@ -0,0 +1,2 @@ +FOR /F "tokens=*" %%g IN ('call ..\..\bazel.bat info output_base --ui_event_filters=-INFO') do (SET BAZEL_EXTERNAL=%%g/external) +set PATH=%BAZEL_EXTERNAL%\rust_windows_x86_64\bin;%PATH% diff --git a/scripts/copyright_headers.py b/scripts/copyright_headers.py index ef4ebf672..5cb1dfc06 100644 --- a/scripts/copyright_headers.py +++ b/scripts/copyright_headers.py @@ -14,6 +14,8 @@ nonstandard_header = { "python/pyqt/install.py", "qt/aqt/mpv.py", "qt/aqt/winpaths.py", + "qt/package/build.rs", + "qt/package/src/main.rs", } ignored_folders = [ diff --git a/scripts/docker/Dockerfile.amd64 b/scripts/docker/Dockerfile.amd64 index 6921c278a..51e57df4e 100644 --- a/scripts/docker/Dockerfile.amd64 +++ b/scripts/docker/Dockerfile.amd64 @@ -6,6 +6,7 @@ ARG gid=1000 RUN apt-get update \ && apt-get install --yes --no-install-recommends \ + autoconf \ bash \ ca-certificates \ curl \ @@ -33,6 +34,7 @@ RUN apt-get update \ libxrandr2 \ libxrender1 \ libxtst6 \ + make \ pkg-config \ portaudio19-dev \ rsync \ diff --git a/scripts/docker/Dockerfile.arm64 b/scripts/docker/Dockerfile.arm64 index 06c3525ec..56f6f00da 100644 --- a/scripts/docker/Dockerfile.arm64 +++ b/scripts/docker/Dockerfile.arm64 @@ -6,6 +6,7 @@ ARG gid=1000 RUN apt-get update \ && apt-get install --yes --no-install-recommends \ + autoconf \ bash \ ca-certificates \ curl \ @@ -33,6 +34,7 @@ RUN apt-get update \ libxrandr2 \ libxrender1 \ libxtst6 \ + make \ pkg-config \ portaudio19-dev \ rsync \ diff --git a/scripts/docker/build-entrypoint b/scripts/docker/build-entrypoint index 61eab413c..6a97fbc20 100644 --- a/scripts/docker/build-entrypoint +++ b/scripts/docker/build-entrypoint @@ -3,8 +3,10 @@ set -e rm -rf bazel-dist -bazel --output_user_root=bazel-docker/root \ - build -c opt dist --symlink_prefix=bazel-docker/links/ \ +bazel build -c opt dist --symlink_prefix=bazel-docker/links/ \ --experimental_no_product_name_out_symlink tar xvf bazel-docker/links/bin/dist.tar -bazel --output_user_root=bazel-docker/root shutdown +if [ "$PACKAGE" != "" ]; then + (cd qt/package && ./build.sh) +fi +bazel shutdown diff --git a/scripts/docker/build.sh b/scripts/docker/build.sh index becfd5f7b..2c3968b55 100755 --- a/scripts/docker/build.sh +++ b/scripts/docker/build.sh @@ -21,6 +21,6 @@ export DOCKER_BUILDKIT=1 docker build --tag ankibuild --file scripts/docker/Dockerfile.$arch \ --build-arg uid=$(id -u) --build-arg gid=$(id -g) \ scripts/docker -docker run --rm -it \ +docker run --rm -it -e PACKAGE=$PACKAGE \ --mount type=bind,source="$(pwd)",target=/code \ ankibuild diff --git a/scripts/python b/scripts/python index 4fc4de93c..242f2e485 100755 --- a/scripts/python +++ b/scripts/python @@ -1,3 +1,3 @@ #!/bin/bash -bazel run python -- $* +bazel run python --ui_event_filters=-INFO -- $* diff --git a/scripts/python.bat b/scripts/python.bat new file mode 100755 index 000000000..44d6a5d3c --- /dev/null +++ b/scripts/python.bat @@ -0,0 +1 @@ +call bazel run python --ui_event_filters=-INFO -- %* From 568dbe798a63bb36551eb3851b7496b0fa4ca437 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 28 Oct 2021 18:57:21 +1000 Subject: [PATCH 3/3] make console optional on Windows --- qt/package/Cargo.lock | 13 ++++++++++++ qt/package/Cargo.toml | 5 ++++- qt/package/rustfmt.toml | 4 ++++ qt/package/src/anki.rs | 35 +++++++++++++++++++++++++++++++++ qt/package/src/main.rs | 6 ++++-- qt/package/win/anki-console.bat | 5 +++++ 6 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 qt/package/rustfmt.toml create mode 100644 qt/package/src/anki.rs create mode 100644 qt/package/win/anki-console.bat diff --git a/qt/package/Cargo.lock b/qt/package/Cargo.lock index a1518ebe7..f20266efd 100644 --- a/qt/package/Cargo.lock +++ b/qt/package/Cargo.lock @@ -17,9 +17,12 @@ version = "0.1.0" dependencies = [ "embed-resource", "jemallocator", + "libc", + "libc-stdhandle", "mimalloc", "pyembed", "snmalloc-rs", + "winapi", ] [[package]] @@ -204,6 +207,16 @@ version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" +[[package]] +name = "libc-stdhandle" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dac2473dc28934c5e0b82250dab231c9d3b94160d91fe9ff483323b05797551" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "libmimalloc-sys" version = "0.1.22" diff --git a/qt/package/Cargo.toml b/qt/package/Cargo.toml index 2403cc008..e5be3f0cb 100644 --- a/qt/package/Cargo.toml +++ b/qt/package/Cargo.toml @@ -4,7 +4,10 @@ version = "0.1.0" build = "build.rs" edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[target.'cfg(windows)'.dependencies] +winapi = {version = "0.3", features = ["wincon"]} +libc = "0.2" +libc-stdhandle = "=0.1.0" [dependencies.pyembed] git = "https://github.com/ankitects/PyOxidizer.git" diff --git a/qt/package/rustfmt.toml b/qt/package/rustfmt.toml new file mode 100644 index 000000000..3c812a2b9 --- /dev/null +++ b/qt/package/rustfmt.toml @@ -0,0 +1,4 @@ +# this is not supported on stable Rust, and is ignored by the Bazel rules; it is only +# useful for manual invocation with 'cargo +nightly fmt' +imports_granularity = "Crate" +group_imports = "StdExternalCrate" diff --git a/qt/package/src/anki.rs b/qt/package/src/anki.rs new file mode 100644 index 000000000..3cf960028 --- /dev/null +++ b/qt/package/src/anki.rs @@ -0,0 +1,35 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +pub(super) fn init() { + #[cfg(target_os = "windows")] + attach_console(); + + println!("Anki starting..."); +} + +/// If parent process has a console (eg cmd.exe), redirect our output there. +#[cfg(target_os = "windows")] +fn attach_console() { + use std::ffi::CString; + + use libc_stdhandle::*; + use winapi::um::wincon; + + let console_attached = unsafe { wincon::AttachConsole(wincon::ATTACH_PARENT_PROCESS) }; + if console_attached == 0 { + return; + } + + let conin = CString::new("CONIN$").unwrap(); + let conout = CString::new("CONOUT$").unwrap(); + let r = CString::new("r").unwrap(); + let w = CString::new("w").unwrap(); + + // Python uses the CRT for I/O, and it requires the descriptors are reopened. + unsafe { + libc::freopen(conin.as_ptr(), r.as_ptr(), stdin()); + libc::freopen(conout.as_ptr(), w.as_ptr(), stdout()); + libc::freopen(conout.as_ptr(), w.as_ptr(), stderr()); + } +} diff --git a/qt/package/src/main.rs b/qt/package/src/main.rs index 6b4084e8a..d424d4f26 100644 --- a/qt/package/src/main.rs +++ b/qt/package/src/main.rs @@ -3,7 +3,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -#![windows_subsystem = "console"] +#![windows_subsystem = "windows"] + +mod anki; use pyembed::{MainPythonInterpreter, OxidizedPythonInterpreterConfig}; @@ -14,7 +16,7 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; include!(env!("DEFAULT_PYTHON_CONFIG_RS")); fn main() { - println!("Anki starting..."); + anki::init(); let exit_code = { let config: OxidizedPythonInterpreterConfig = default_python_config(); diff --git a/qt/package/win/anki-console.bat b/qt/package/win/anki-console.bat new file mode 100644 index 000000000..7b488cdd9 --- /dev/null +++ b/qt/package/win/anki-console.bat @@ -0,0 +1,5 @@ +@echo off +anki +pause + +