diff --git a/.bazelignore b/.bazelignore
index 3c3629e64..1c30ea3ed 100644
--- a/.bazelignore
+++ b/.bazelignore
@@ -1 +1,2 @@
node_modules
+.bazel
diff --git a/.bazelrc b/.bazelrc
index dc09b785b..81430cc64 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -8,7 +8,7 @@ build --enable_runfiles
build:windows --build_python_zip=false
# record version/build hash
-build --workspace_status_command='bash ./scripts/status.sh'
+build --workspace_status_command='bash ./tools/status.sh'
# run clippy when compiling rust in test mode
test --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect --output_groups=+clippy_checks
@@ -31,7 +31,9 @@ build:windows --worker_quit_after_build
# place convenience symlinks inside a single folder for easier exclusion in IDEs
build --symlink_prefix=.bazel/
-build --experimental_no_product_name_out_symlink
+
+# if (auto-created) windows.bazelrc exists, import it
+try-import %workspace%/windows.bazelrc
# allow extra user customizations in a separate file
# (see .user.bazelrc for an example)
diff --git a/.bazelversion b/.bazelversion
index fae6e3d04..0062ac971 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-4.2.1
+5.0.0
diff --git a/scripts/docker/Dockerfile.amd64 b/.buildkite/linux/docker/Dockerfile.amd64
similarity index 53%
rename from scripts/docker/Dockerfile.amd64
rename to .buildkite/linux/docker/Dockerfile.amd64
index 51e57df4e..2e745396d 100644
--- a/scripts/docker/Dockerfile.amd64
+++ b/.buildkite/linux/docker/Dockerfile.amd64
@@ -1,13 +1,17 @@
-FROM debian:10-slim
+FROM python:3.9-slim-buster
ARG DEBIAN_FRONTEND="noninteractive"
-ARG uid=1000
-ARG gid=1000
-RUN apt-get update \
+RUN useradd -d /state -m -u 998 user
+
+RUN apt-get update && apt install --yes gnupg ca-certificates && \
+ apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 32A37959C2FA5C3C99EFBC32A79206696452D198 \
+ && echo "deb https://apt.buildkite.com/buildkite-agent stable main" > /etc/apt/sources.list.d/buildkite-agent.list \
+ && apt-get update \
&& apt-get install --yes --no-install-recommends \
autoconf \
bash \
+ buildkite-agent \
ca-certificates \
curl \
findutils \
@@ -23,6 +27,7 @@ RUN apt-get update \
libgstreamer-plugins-base1.0 \
libgstreamer1.0-0 \
libnss3 \
+ libpulse-mainloop-glib0 \
libpulse-mainloop-glib0 \
libssl-dev \
libxcomposite1 \
@@ -37,23 +42,23 @@ RUN apt-get update \
make \
pkg-config \
portaudio19-dev \
+ python3-dev \
rsync \
+ zstd \
&& rm -rf /var/lib/apt/lists/*
-RUN curl -L https://github.com/bazelbuild/bazelisk/releases/download/v1.10.1/bazelisk-linux-amd64 \
+
+RUN curl -L https://github.com/bazelbuild/bazelisk/releases/download/v1.7.4/bazelisk-linux-amd64 \
-o /usr/local/bin/bazel \
&& chmod +x /usr/local/bin/bazel
-RUN mkdir -p /code/bazel-docker/home && \
- echo groupadd -g ${gid} user && \
- useradd -d /code/bazel-docker/home -m -u ${uid} user && \
- chown -R user.user /code
-
RUN ln -sf /usr/bin/python3 /usr/bin/python
-USER user
-COPY build-entrypoint /tmp
-WORKDIR /code
-ENV XDG_CACHE_HOME=/code/bazel-docker/home
+RUN mkdir -p /etc/buildkite-agent/hooks && chown -R user /etc/buildkite-agent
-ENTRYPOINT ["/bin/bash", "/tmp/build-entrypoint"]
+COPY buildkite.cfg /etc/buildkite-agent/buildkite-agent.cfg
+COPY environment /etc/buildkite-agent/hooks/environment
+
+USER user
+WORKDIR /code/buildkite
+ENTRYPOINT ["/usr/bin/buildkite-agent", "start"]
diff --git a/scripts/docker/Dockerfile.arm64 b/.buildkite/linux/docker/Dockerfile.arm64
similarity index 57%
rename from scripts/docker/Dockerfile.arm64
rename to .buildkite/linux/docker/Dockerfile.arm64
index 56f6f00da..2067c9a97 100644
--- a/scripts/docker/Dockerfile.arm64
+++ b/.buildkite/linux/docker/Dockerfile.arm64
@@ -1,13 +1,18 @@
FROM debian:11-slim
ARG DEBIAN_FRONTEND="noninteractive"
-ARG uid=1000
-ARG gid=1000
+ENV PYTHON_SITE_PACKAGES=/usr/lib/python3/dist-packages/
-RUN apt-get update \
+RUN useradd -d /state -m -u 998 user
+
+RUN apt-get update && apt install --yes gnupg ca-certificates && \
+ apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 32A37959C2FA5C3C99EFBC32A79206696452D198 \
+ && echo "deb https://apt.buildkite.com/buildkite-agent stable main" > /etc/apt/sources.list.d/buildkite-agent.list \
+ && apt-get update \
&& apt-get install --yes --no-install-recommends \
autoconf \
bash \
+ buildkite-agent \
ca-certificates \
curl \
findutils \
@@ -23,6 +28,7 @@ RUN apt-get update \
libgstreamer-plugins-base1.0 \
libgstreamer1.0-0 \
libnss3 \
+ libpulse-mainloop-glib0 \
libpulse-mainloop-glib0 \
libssl-dev \
libxcomposite1 \
@@ -37,12 +43,13 @@ RUN apt-get update \
make \
pkg-config \
portaudio19-dev \
+ python3-dev \
rsync \
# -- begin only required for arm64/debian11
clang-format \
python-is-python3 \
python3-pyqt5.qtwebengine \
- # -- end only required for arm64/debian11
+ # -- end only required for arm64/debian11
&& rm -rf /var/lib/apt/lists/*
@@ -50,12 +57,13 @@ RUN curl -L https://github.com/bazelbuild/bazelisk/releases/download/v1.10.1/baz
-o /usr/local/bin/bazel \
&& chmod +x /usr/local/bin/bazel
-RUN echo groupadd -g ${gid} user && useradd -d /code/bazel-docker/home -m -u ${uid} user
+RUN ln -sf /usr/bin/python3 /usr/bin/python
+
+RUN mkdir -p /etc/buildkite-agent/hooks && chown -R user /etc/buildkite-agent
+
+COPY buildkite.cfg /etc/buildkite-agent/buildkite-agent.cfg
+COPY environment /etc/buildkite-agent/hooks/environment
USER user
-COPY build-entrypoint /tmp
-WORKDIR /code
-ENV XDG_CACHE_HOME=/code/bazel-docker/home
-ENV PYTHON_SITE_PACKAGES=/usr/lib/python3/dist-packages/
-
-ENTRYPOINT ["/bin/bash", "/tmp/build-entrypoint"]
+WORKDIR /code/buildkite
+ENTRYPOINT ["/usr/bin/buildkite-agent", "start"]
diff --git a/.buildkite/linux/docker/buildkite.cfg b/.buildkite/linux/docker/buildkite.cfg
new file mode 100644
index 000000000..3b5345c46
--- /dev/null
+++ b/.buildkite/linux/docker/buildkite.cfg
@@ -0,0 +1,6 @@
+name="lin-ci"
+tags="queue=lin-ci"
+build-path="/state/build"
+hooks-path="/etc/buildkite-agent/hooks"
+no-plugins=true
+no-local-hooks=true
diff --git a/.buildkite/linux/docker/environment b/.buildkite/linux/docker/environment
new file mode 100755
index 000000000..9a43ce00b
--- /dev/null
+++ b/.buildkite/linux/docker/environment
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+if [[ "${BUILDKITE_COMMAND}" != ".buildkite/linux/entrypoint" &&
+ "${BUILDKITE_COMMAND}" != ".buildkite/linux/release-entrypoint" ]]; then
+ echo "Command not allowed: ${BUILDKITE_COMMAND}"
+ exit 1
+fi
diff --git a/.buildkite/linux/docker/run.sh b/.buildkite/linux/docker/run.sh
new file mode 100755
index 000000000..494c4c70e
--- /dev/null
+++ b/.buildkite/linux/docker/run.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+# use './run.sh serve' to daemonize
+
+set -e
+
+if [ "$1" = "serve" ]; then
+ extra_args="-d --restart always"
+else
+ extra_args="-it"
+fi
+
+if [ $(uname -m) = "aarch64" ]; then
+ arch=arm64
+else
+ arch=amd64
+fi
+
+DOCKER_BUILDKIT=1 docker build -f Dockerfile.${arch} --tag linci .
+
+if docker container inspect linci > /dev/null 2>&1; then
+ docker stop linci || true
+ docker container rm linci
+fi
+
+docker run $extra_args \
+ --name linci \
+ -v ci-state:/state \
+ -e BUILDKITE_AGENT_TOKEN \
+ -e BUILDKITE_AGENT_TAGS \
+ linci
diff --git a/.buildkite/linux/entrypoint b/.buildkite/linux/entrypoint
index 9ddba1eb5..bee05ae7e 100755
--- a/.buildkite/linux/entrypoint
+++ b/.buildkite/linux/entrypoint
@@ -17,7 +17,7 @@ test -e /state/node_modules && mv /state/node_modules .
$BAZEL test $BUILDARGS ... //rslib/linkchecker
echo "--- Running lints"
-python scripts/copyright_headers.py
+python tools/copyright_headers.py
echo "--- Cleanup"
# if tests succeed, back up node_modules folder
diff --git a/.buildkite/linux/release-entrypoint b/.buildkite/linux/release-entrypoint
new file mode 100755
index 000000000..496a1bd8e
--- /dev/null
+++ b/.buildkite/linux/release-entrypoint
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+set -e
+
+# move existing node_modules into tree
+test -e /state/node_modules && mv /state/node_modules .
+
+if [ $(uname -m) = "aarch64" ]; then
+ ./tools/build
+else
+ ./tools/bundle
+fi
+
+rm -rf /state/dist
+mv .bazel/out/dist /state
+
+# if tests succeed, back up node_modules folder
+mv node_modules /state/
diff --git a/.buildkite/mac/entrypoint b/.buildkite/mac/entrypoint
index 4eafed2c8..d0f05c149 100755
--- a/.buildkite/mac/entrypoint
+++ b/.buildkite/mac/entrypoint
@@ -15,7 +15,7 @@ test -e $STATE/node_modules && mv $STATE/node_modules .
$BAZEL test $BUILDARGS ...
echo "--- Building wheels"
-$BAZEL build dist
+$BAZEL build wheels
# if tests succeed, back up node_modules folder
mv node_modules $STATE/
diff --git a/.gitignore b/.gitignore
index b59d2f5af..350f526f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,10 @@
__pycache__
.DS_Store
-/bazel-*
anki.prof
target
-user.bazelrc
+/user.bazelrc
.dmypy.json
-rust-project.json
node_modules
-.idea/
-.bazel
+/.idea/
+/.bazel
+/windows.bazelrc
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 993214aef..53e657d93 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,19 +4,21 @@
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/*/**": true,
- ".bazel/**": true
+ ".bazel/**": true,
+ "dist/**": true
},
"python.analysis.extraPaths": ["./pylib"],
"python.formatting.provider": "black",
"rust-analyzer.cargo.runBuildScripts": true,
"rust-analyzer.checkOnSave.allTargets": false,
- "rust-analyzer.files.excludeDirs": [".bazel", "node_modules"],
+ "rust-analyzer.files.excludeDirs": [".bazel", "node_modules", "dist"],
"rust-analyzer.procMacro.enable": true,
// this formats 'use' blocks in a nicer way, but requires you to run
// 'rustup install nightly'.
"rust-analyzer.rustfmt.extraArgs": ["+nightly"],
"search.exclude": {
"**/node_modules": true,
- ".bazel/**": true
+ ".bazel/**": true,
+ "dist/**": true
}
}
diff --git a/BUILD.bazel b/BUILD.bazel
index 79ba87f85..d6dc535ba 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -12,24 +12,23 @@ genrule(
srcs = ["//:defs.bzl"],
outs = ["buildinfo.txt"],
cmd = select({
- "release": "$(location //scripts:buildinfo) $(location //:defs.bzl) bazel-out/stable-status.txt release > $@",
- "//conditions:default": "$(location //scripts:buildinfo) $(location //:defs.bzl) bazel-out/stable-status.txt devel > $@",
+ "release": "$(location //tools:buildinfo) $(location //:defs.bzl) bazel-out/stable-status.txt release > $@",
+ "//conditions:default": "$(location //tools:buildinfo) $(location //:defs.bzl) bazel-out/stable-status.txt devel > $@",
}),
stamp = 1,
tools = [
- "//scripts:buildinfo",
+ "//tools:buildinfo",
],
visibility = ["//visibility:public"],
)
pkg_tar(
- name = "dist",
+ name = "wheels",
srcs = [
"//pylib/anki:wheel",
"//qt/aqt:wheel",
],
mode = "0644",
- package_dir = "bazel-dist",
tags = ["manual"],
)
diff --git a/Cargo.toml b/Cargo.toml
index f63c742ec..e8a6b708f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,7 @@ license = "AGPL-3.0-or-later"
[workspace]
members = ["rslib", "rslib/i18n", "rslib/i18n_helpers", "rslib/linkchecker", "pylib/rsbridge"]
-exclude = ["qt/package"]
+exclude = ["qt/bundle"]
[lib]
# dummy top level for tooling
diff --git a/bazel.bat b/bazel.bat
deleted file mode 100755
index 3875a5917..000000000
--- a/bazel.bat
+++ /dev/null
@@ -1,2 +0,0 @@
-@set PATH=c:\msys64\usr\bin;c:\python;%PATH%
-\bazel\bazel --output_user_root=\bazel\anki %*
diff --git a/cargo/README.md b/cargo/README.md
index bec55c40e..1fe6acbf4 100644
--- a/cargo/README.md
+++ b/cargo/README.md
@@ -1,7 +1,7 @@
This folder integrates Rust crates.io fetching into Bazel.
To add or update dependencies, ensure a local Rust environment is available
-(eg `source scripts/cargo-env`), then install cargo-raze:
+(eg `source tools/cargo-env`), then install cargo-raze:
```
cargo install cargo-raze --version 0.14.1
diff --git a/docs/development.md b/docs/development.md
index 161202ea1..96eec4b94 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -56,13 +56,13 @@ Run the following command to create Python packages:
On Mac/Linux:
```
-./scripts/build
+./tools/build
```
On Windows:
```
-.\scripts\build.bat
+.\tools\build.bat
```
The generated wheel paths will be printed as the build completes.
@@ -71,7 +71,7 @@ Follow the steps [on the beta site](https://betas.ankiweb.net/#via-pypipip), but
`pip install --upgrade --pre aqt[qt6]` line with something like:
```
-pyenv/bin/pip install --upgrade bazel-dist/*.whl
+pyenv/bin/pip install --upgrade dist/*.whl
```
(On Windows you'll need to list out the filenames manually instead of using a wildcard).
@@ -94,36 +94,14 @@ The build process will download about a gigabyte of dependencies, and produce
about 6 gigabytes of temporary files. Once you've created the wheels, you can
remove the other files to free up space if you wish.
-- `bazel clean --expunge` will remove the generated files, freeing up most
- of the space. The files are usualy stored in a subdir of ~/.cache/bazel/
-- `rm -rf ~/.cache/bazel*` will remove the cached downloads as well, requiring
- them to be redownloaded if you want to build again.
+- `bazel clean --expunge` will remove the generated Bazel files, freeing up
+ most of the space. The files are usualy stored in a subdir of
+ `~/.cache/bazel` or `\bazel\anki`
+- `rm -rf ~/.cache/bazel*` or `\bazel\anki` will remove cached downloads as
+ well, requiring them to be redownloaded if you want to build again.
- `rm -rf ~/.cache/{yarn,pip}` will remove the shared pip and yarn caches that
other apps may be using as well.
-## Building with Docker
-
-Linux users can build using the instructions above, or they can optionally [build
-via Docker](../scripts/docker/README.md).
-
-On Linux, the generated Anki wheel will have a filename like:
-
- anki-2.1.49-cp39-abi3-manylinux_2_31_aarch64.whl
-
-The 2_31 part means that the wheel requires glibc 2.31 or later. If you have
-built the wheel on a machine with an older glibc version, you will get an error
-if you try to install the wheel:
-
- ERROR: No matching distribution found for anki
-
-To avoid the error, you can rename the .whl file to match your glibc version.
-
-If you still get the error, another possibility is that you are trying to
-install with an old version of Python - 3.9 or later is required.
-
-On ARM Linux, please see the instructions in the pre-built wheels section about
-a system PyQt, and the notes at the bottom of [Linux](./linux.md).
-
## Running tests
You can run all tests at once. From the top level project folder:
@@ -151,7 +129,7 @@ On Mac/Linux, after installing 'fswatch', you can run mypy on
each file save automatically with:
```
-./scripts/mypy-watch
+./tools/mypy-watch
```
## Fixing formatting
@@ -233,6 +211,12 @@ in the collection2.log file will also be printed on stdout.
If ANKI_PROFILE_CODE is set, Python profiling data will be written on exit.
+# Binary Bundles
+
+Anki's official binary packages are created with `tools/bundle`. The script was created specifically
+for the official builds, and is provided as-is; we are unfortunately not able to provide assistance with
+any issues you may run into when using it.
+
## Mixing development and study
You may wish to create a separate profile with File>Switch Profile for use
diff --git a/docs/docker/Dockerfile b/docs/docker/Dockerfile
index cf41a9091..6682f70f6 100644
--- a/docs/docker/Dockerfile
+++ b/docs/docker/Dockerfile
@@ -17,12 +17,12 @@ RUN curl -fsSL https://github.com/bazelbuild/bazelisk/releases/download/v1.7.4/b
WORKDIR /opt/anki
COPY . .
# Build python wheels.
-RUN ./scripts/build
+RUN ./tools/build
# Install pre-compiled Anki.
FROM python:${PYTHON_VERSION}-slim as installer
WORKDIR /opt/anki/
-COPY --from=build /opt/anki/bazel-dist/ wheels/
+COPY --from=build /opt/anki/wheels/ wheels/
# Use virtual environment.
RUN python -m venv venv \
&& ./venv/bin/python -m pip install --no-cache-dir setuptools wheel \
@@ -35,29 +35,29 @@ ENV PATH=/opt/anki/venv/bin:$PATH
# Install run-time dependencies.
RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
- libasound2 \
- libdbus-1-3 \
- libfontconfig1 \
- libfreetype6 \
- libgl1 \
- libglib2.0-0 \
- libnss3 \
- libxcb-icccm4 \
- libxcb-image0 \
- libxcb-keysyms1 \
- libxcb-randr0 \
- libxcb-render-util0 \
- libxcb-shape0 \
- libxcb-xinerama0 \
- libxcb-xkb1 \
- libxcomposite1 \
- libxcursor1 \
- libxi6 \
- libxkbcommon0 \
- libxkbcommon-x11-0 \
- libxrandr2 \
- libxrender1 \
- libxtst6 \
+ libasound2 \
+ libdbus-1-3 \
+ libfontconfig1 \
+ libfreetype6 \
+ libgl1 \
+ libglib2.0-0 \
+ libnss3 \
+ libxcb-icccm4 \
+ libxcb-image0 \
+ libxcb-keysyms1 \
+ libxcb-randr0 \
+ libxcb-render-util0 \
+ libxcb-shape0 \
+ libxcb-xinerama0 \
+ libxcb-xkb1 \
+ libxcomposite1 \
+ libxcursor1 \
+ libxi6 \
+ libxkbcommon0 \
+ libxkbcommon-x11-0 \
+ libxrandr2 \
+ libxrender1 \
+ libxtst6 \
&& rm -rf /var/lib/apt/lists/*
# Add non-root user.
RUN useradd --create-home anki
diff --git a/docs/docker/README.md b/docs/docker/README.md
index f57232d5c..a9f233bf9 100644
--- a/docs/docker/README.md
+++ b/docs/docker/README.md
@@ -1,14 +1,17 @@
-# Anki in Docker
+# Building and running Anki in Docker
-This is an example of how you can build and run Anki from inside Docker. This
-approach keeps everything inside Docker images, and sends the GUI to an X11
-display over TCP/IP. This approach keeps things tidy, so may be a good choice
-for if you wish to build Anki irregularly and don't want to build it outside of
-Docker.
+This is an example Dockerfile contributed by an Anki user, which shows how Anki
+can be both built and run from within a container. It works by streaming the GUI
+over an X11 socket.
-It takes longer to build after small changes however, so for development, if you
-wish to use Docker, the approach [in the build
-scripts](../../scripts/docker/README.md) may be more appropriate.
+Building and running Anki within a container has the advantage of fully isolating
+the build products and runtime dependencies from the rest of your system, but it is
+a somewhat niche approach, with some downsides such as an inability to display natively
+on Wayland, and a lack of integration with desktop icons/filetypes. But even if you
+do not use this Dockerfile as-is, you may find it useful as a reference.
+
+Anki's Linux CI is also implemented with Docker, and the Dockerfiles for that may
+also be useful for reference - they can be found in `.buildkite/linux/docker`.
# Build the Docker image
diff --git a/docs/editing.md b/docs/editing.md
index 8ff351a6a..6000dac72 100644
--- a/docs/editing.md
+++ b/docs/editing.md
@@ -34,7 +34,7 @@ run 'rustup install nightly'.
Code completion partly depends on files that are generated as part of the
regular build process, so for things to work correctly, use './run' or
-'scripts/build' prior to using code completion.
+'tools/build' prior to using code completion.
## PyCharm/IntelliJ
diff --git a/docs/linux.md b/docs/linux.md
index 87ac069c5..08dd56d50 100644
--- a/docs/linux.md
+++ b/docs/linux.md
@@ -6,8 +6,9 @@ These instructions are written for Debian/Ubuntu; adjust for your distribution.
Some extra notes have been provided by a forum member:
https://forums.ankiweb.net/t/guide-how-to-build-and-run-anki-from-source-with-xubuntu-20-04/12865
-You can see a full list of requirements by looking at the [Dockerfiles](../scripts/docker/README.md)
-in the scripts folder.
+You can see a full list of buildtime and runtime requirements by looking at the
+[Dockerfiles](../.buildkite/linux/docker/Dockerfile.amd64) used to build the
+official releases.
Glibc is required - if you are on a distro like Alpine that uses musl, you'll need
to contribute fixes to the upstream [Rust rules](https://github.com/bazelbuild/rules_rust/issues/390),
@@ -101,7 +102,7 @@ to compile, but will mean Anki will run considerably slower.
To run Anki in optimized mode, use:
```
-./scripts/runopt
+./tools/runopt
```
## ARM64 support
@@ -122,7 +123,7 @@ Note: the trailing slash at the end is required.
There are a few things to be aware of:
-- You should use ./run and not scripts/run-qt5\*, even if your system libraries are Qt5.
+- You should use ./run and not tools/run-qt5\*, even if your system libraries are Qt5.
- If your system libraries are Qt5, when creating an aqt wheel, the wheel will not work
on Qt6 environments.
- Some of the tests only work with PyQt6, and will show failures when run under PyQt5.
diff --git a/docs/mac.md b/docs/mac.md
index 0dd71e3fe..6bcce0db2 100644
--- a/docs/mac.md
+++ b/docs/mac.md
@@ -7,14 +7,12 @@
Install the latest XCode from the App Store. Open it at least once
so it installs the command line tools.
-**Homebrew & Homebrew Deps**:
-
-Install Homebrew from
-
-Then install Bazel:
+**Bazelisk**:
```
-$ brew install bazelisk
+$ curl -L https://github.com/bazelbuild/bazelisk/releases/download/v1.11.0/bazelisk-darwin -o bazel \
+ && chmod +x bazel \
+ && sudo mv bazel /usr/local/bin
```
**Python**:
@@ -61,7 +59,7 @@ to compile, but will mean Anki will run considerably slower.
To run Anki in optimized mode, use:
```
-./scripts/runopt
+./tools/runopt
```
## More
diff --git a/docs/syncserver.md b/docs/syncserver.md
index 23c2afe34..aa23713a6 100644
--- a/docs/syncserver.md
+++ b/docs/syncserver.md
@@ -26,7 +26,7 @@ Things to be aware of:
If you run Anki from git, you can run a sync server with:
```
-./scripts/runopt --syncserver
+./tools/runopt --syncserver
```
## From a packaged build
diff --git a/docs/windows.md b/docs/windows.md
index 42c804694..f123860b3 100644
--- a/docs/windows.md
+++ b/docs/windows.md
@@ -74,7 +74,7 @@ to compile, but will mean Anki will run considerably slower.
To run Anki in optimized mode, use:
```
-.\scripts\runopt
+.\tools\runopt
```
## More
@@ -82,7 +82,7 @@ To run Anki in optimized mode, use:
For info on running tests, building wheels and so on, please see
[Development](./development.md).
-Note that where the instructions on that page say "bazel", please use ".\bazel"
-instead. This runs bazel.bat inside the Anki source folder, instead of
-calling Bazel directly. This takes care of setting up the path and output folder
-correctly, which avoids issues with long path names.
+When you run a script like .\run, MSYS and bazel will automatically be added to
+the path, and Bazel will be configured to output build products into
+\bazel\anki. If you want to directly invoke bazel before having run any of the
+.bat files in this repo, please run tools\setup-env first.
diff --git a/pyrightconfig.json b/pyrightconfig.json
index e3fbcd57b..e58a21fb2 100644
--- a/pyrightconfig.json
+++ b/pyrightconfig.json
@@ -1,3 +1,3 @@
{
- "exclude": ["**/node_modules", ".bazel"]
+ "exclude": ["**/node_modules", ".bazel", "dist"]
}
diff --git a/python/licenses.sh b/python/licenses.sh
index 273edf99e..a7985a429 100755
--- a/python/licenses.sh
+++ b/python/licenses.sh
@@ -14,10 +14,10 @@ python -m venv venv
../bazel.bat --output_base=/c/bazel/anki/base build //pylib/anki:wheel //qt/aqt:wheel
# install wheels, bound to constrained versions
-venv/scripts/pip install -c requirements.txt ../bazel-bin/pylib/anki/*.whl ../bazel-bin/qt/aqt/*.whl pip-licenses
+venv/tools/pip install -c requirements.txt ../bazel-bin/pylib/anki/*.whl ../bazel-bin/qt/aqt/*.whl pip-licenses
# dump licenses - ptable is a pip-licenses dep
-venv/scripts/pip-licenses --format=json --ignore-packages anki aqt pip-license PTable > licenses.json
+venv/tools/pip-licenses --format=json --ignore-packages anki aqt pip-license PTable > licenses.json
# clean up
rm -rf venv
diff --git a/qt/package/.cargo/config b/qt/bundle/.cargo/config
similarity index 100%
rename from qt/package/.cargo/config
rename to qt/bundle/.cargo/config
diff --git a/qt/package/Cargo.lock b/qt/bundle/Cargo.lock
similarity index 100%
rename from qt/package/Cargo.lock
rename to qt/bundle/Cargo.lock
diff --git a/qt/package/Cargo.toml b/qt/bundle/Cargo.toml
similarity index 95%
rename from qt/package/Cargo.toml
rename to qt/bundle/Cargo.toml
index fcc42cbe9..ee591b411 100644
--- a/qt/package/Cargo.toml
+++ b/qt/bundle/Cargo.toml
@@ -11,6 +11,7 @@ libc-stdhandle = "=0.1.0"
[dependencies.pyembed]
git = "https://github.com/ankitects/PyOxidizer.git"
+# when changing this, pyoxidizer in /repos.bzl needs to be updated as well
rev = "eb26dd7cd1290de6503869f3d719eabcec45e139"
default-features = false
diff --git a/qt/bundle/build.py b/qt/bundle/build.py
new file mode 100644
index 000000000..99d253b31
--- /dev/null
+++ b/qt/bundle/build.py
@@ -0,0 +1,395 @@
+# Copyright: Ankitects Pty Ltd and contributors
+# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+
+from __future__ import annotations
+
+import glob
+import os
+import platform
+import re
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+is_win = sys.platform == "win32"
+is_mac = sys.platform == "darwin"
+
+workspace = Path(sys.argv[1])
+bazel_external = Path(sys.argv[2])
+
+
+def with_exe_extension(program: str) -> str:
+ if is_win:
+ return program + ".exe"
+ else:
+ return program
+
+
+output_root = workspace / ".bazel" / "out" / "build"
+dist_folder = output_root / ".." / "dist"
+venv = output_root / f"venv-{platform.machine()}"
+build_folder = output_root / f"build-{platform.machine()}"
+cargo_target = output_root / f"target-{platform.machine()}"
+artifacts = output_root / "artifacts"
+pyo3_config = output_root / "pyo3-build-config-file.txt"
+pyoxidizer_folder = bazel_external / "pyoxidizer"
+arm64_protobuf_wheel = bazel_external / "protobuf_wheel_mac_arm64"
+pyoxidizer_binary = cargo_target / "release" / with_exe_extension("pyoxidizer")
+
+for path in dist_folder.glob("*.zst"):
+ path.unlink()
+
+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"
+pyqt6_folder_path = bazel_external / "pyqt6" / "PyQt6"
+extra_linux_deps = bazel_external / "bundle_extras_linux_amd64"
+extra_qt5_linux_plugins = extra_linux_deps / "qt5"
+extra_qt6_linux_plugins = extra_linux_deps / "qt6"
+is_lin = False
+arm64_linux = arm64_mac = False
+if is_win:
+ os.environ["TARGET"] = "x86_64-pc-windows-msvc"
+elif sys.platform.startswith("darwin"):
+ if platform.machine() == "arm64":
+ arm64_mac = True
+ 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:
+ is_lin = True
+ if platform.machine() == "x86_64":
+ os.environ["TARGET"] = "x86_64-unknown-linux-gnu"
+ else:
+ os.environ["TARGET"] = "aarch64-unknown-linux-gnu"
+ pyqt5_folder_name = None
+ pyqt6_folder_path = None
+ arm64_linux = True
+
+if is_win:
+ python_bin_folder = venv / "scripts"
+ os.environ["PATH"] += rf";{os.getenv('USERPROFILE')}\.cargo\bin"
+ cargo_features = "build-mode-prebuilt-artifacts"
+else:
+ python_bin_folder = venv / "bin"
+ # PyOxidizer build depends on a system-installed version of Python,
+ # as the standalone build does not have its config set up properly,
+ # leading to "directory not found for option '-L/install/lib'".
+ # On macOS, after installing a system Python in /usr/local/bin,
+ # make sure /usr/local/bin/python3 is symlinked to /usr/local/bin/python.
+ os.environ["PATH"] = ":".join(
+ ["/usr/local/bin", f"{os.getenv('HOME')}/.cargo/bin", os.getenv("PATH")]
+ )
+ cargo_features = "build-mode-prebuilt-artifacts"
+ if not is_mac or arm64_mac:
+ cargo_features += " global-allocator-jemalloc allocator-jemalloc"
+
+python = python_bin_folder / with_exe_extension("python")
+pip = python_bin_folder / with_exe_extension("pip")
+artifacts_in_build = (
+ build_folder / os.getenv("TARGET") / "release" / "resources" / "extra_files"
+)
+
+
+def build_pyoxidizer():
+ pyoxidizer_folder_mtime = pyoxidizer_folder.stat().st_mtime
+ if (
+ pyoxidizer_binary.exists()
+ and pyoxidizer_binary.stat().st_mtime == pyoxidizer_folder_mtime
+ ):
+ # avoid recompiling if pyoxidizer folder has not changed
+ return
+ subprocess.run(
+ [
+ "cargo",
+ "build",
+ "--release",
+ ],
+ cwd=pyoxidizer_folder,
+ check=True,
+ )
+ os.utime(pyoxidizer_binary, (pyoxidizer_folder_mtime, pyoxidizer_folder_mtime))
+
+
+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)
+ extracted = [
+ line for line in extracted if not arm64_mac or "protobuf" not in line
+ ]
+ f.write("\n".join(extracted))
+ # pypi protobuf lacks C extension on darwin-arm64, so we have to use a version
+ # we built ourselves
+ if arm64_mac:
+ wheels = glob.glob(str(arm64_protobuf_wheel / "*.whl"))
+ subprocess.run(
+ [pip, "install", "--upgrade", "-c", constraints, *wheels], check=True
+ )
+ # install wheels and upgrade any deps
+ wheels = glob.glob(str(workspace / ".bazel" / "out" / "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_binary,
+ "--system-rust",
+ "run-build-script",
+ "build.rs",
+ "--var",
+ "venv",
+ venv,
+ "--var",
+ "build",
+ build_folder,
+ ],
+ 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 | None):
+ if output_folder.exists():
+ shutil.rmtree(output_folder)
+ output_folder.mkdir(parents=True)
+ # PyQt
+ if pyqt_src_path and not is_mac:
+ 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,
+ )
+ if is_lin:
+ if "PyQt5" in str(pyqt_src_path):
+ src = extra_qt5_linux_plugins
+ dest = output_folder / "lib" / "PyQt5" / "Qt5" / "plugins"
+ else:
+ src = extra_qt6_linux_plugins
+ dest = output_folder / "lib" / "PyQt6" / "Qt6" / "plugins"
+ subprocess.run(
+ ["rsync", "-a", str(src) + "/", str(dest) + "/"],
+ 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_lin:
+ resources.append("lin/")
+
+ subprocess.run(
+ [
+ "rsync",
+ "-a",
+ "--delete",
+ "--exclude",
+ "PyQt6",
+ "--exclude",
+ "PyQt5",
+ *resources,
+ adj_path_for_windows_rsync(output_folder) + "/",
+ ],
+ check=True,
+ )
+ # Ensure all files are world-readable
+ if not is_win:
+ subprocess.run(["chmod", "-R", "a+r", output_folder])
+
+
+def anki_version() -> str:
+ with open(workspace / "defs.bzl") as fobj:
+ data = fobj.read()
+ return re.search('^anki_version = "(.*)"$', data, re.MULTILINE).group(1)
+
+
+def annotated_linux_folder_name(variant: str) -> str:
+ components = ["anki", anki_version(), "linux", variant]
+ return "-".join(components)
+
+
+def annotated_mac_dmg_name(variant: str) -> str:
+ if platform.machine() == "arm64":
+ arch = "apple"
+ else:
+ arch = "intel"
+ components = ["anki", anki_version(), "mac", arch, variant]
+ return "-".join(components)
+
+
+def build_bundle(src_path: Path, variant: str) -> None:
+ if is_lin:
+ print("--- Build tarball")
+ build_tarball(src_path, variant)
+ elif is_mac:
+ print("--- Build app bundle")
+ build_app_bundle(src_path, variant)
+
+
+def build_app_bundle(src_path: Path, variant: str) -> None:
+ if arm64_mac:
+ variant = "qt6_arm64"
+ else:
+ variant = f"{variant}_amd64"
+ subprocess.run(
+ ["cargo", "run", variant, src_path, anki_version(), bazel_external],
+ check=True,
+ cwd=workspace / "qt" / "bundle" / "mac",
+ )
+ variant_path = src_path.parent / "app" / variant
+ if os.getenv("NOTARIZE_USER"):
+ subprocess.run(
+ ["python", "mac/notarize.py", "upload", variant_path],
+ check=True,
+ )
+ # note down the dmg name for later
+ open(variant_path / "dmg_name", "w").write(
+ annotated_mac_dmg_name(variant[0:3]) + ".dmg"
+ )
+
+
+def build_tarball(src_path: Path, variant: str) -> None:
+ if not is_lin:
+ return
+ dest_path = src_path.with_name(annotated_linux_folder_name(variant))
+ if dest_path.exists():
+ shutil.rmtree(dest_path)
+ os.rename(src_path, dest_path)
+ print("compress", dest_path.name, "...")
+ subprocess.run(
+ [
+ "tar",
+ "--zstd",
+ "-cf",
+ dist_folder / (dest_path.name + ".tar.zst"),
+ dest_path.name,
+ ],
+ check=True,
+ env=dict(ZSTD_CLEVEL="9"),
+ cwd=dest_path.parent,
+ )
+
+
+def build_windows_installers() -> None:
+ subprocess.run(
+ [
+ "cargo",
+ "run",
+ output_root,
+ bazel_external,
+ Path(__file__).parent,
+ anki_version(),
+ ],
+ check=True,
+ cwd=workspace / "qt" / "bundle" / "win",
+ )
+
+
+print("--- Build PyOxidizer")
+build_pyoxidizer()
+print("--- Install wheels into venv")
+install_wheels_into_venv()
+print("--- Build PyOxidizer artifacts")
+build_artifacts()
+print("--- Build Anki binary")
+build_pkg()
+print("--- Copy binary+resources into folder (Qt6)")
+merge_into_dist(output_root / "std", pyqt6_folder_path)
+build_bundle(output_root / "std", "qt6")
+if pyqt5_folder_name:
+ print("--- Copy binary+resources into folder (Qt5)")
+ merge_into_dist(output_root / "alt", bazel_external / pyqt5_folder_name / "PyQt5")
+ build_bundle(output_root / "alt", "qt5")
+
+if is_win:
+ build_windows_installers()
+
+if is_mac:
+ print("outputs are in .bazel/out/build/{std,alt}")
+ print("dmg can be created with mac/finalize.py dmg")
+else:
+ print("outputs are in .bazel/out/dist/")
diff --git a/qt/package/build.rs b/qt/bundle/build.rs
similarity index 98%
rename from qt/package/build.rs
rename to qt/bundle/build.rs
index 75a6d64c4..000dbd61a 100644
--- a/qt/package/build.rs
+++ b/qt/bundle/build.rs
@@ -104,6 +104,6 @@ fn main() {
// embed manifest and icon
if target_family == "windows" {
- embed_resource::compile("anki-manifest.rc");
+ embed_resource::compile("win/anki-manifest.rc");
}
}
diff --git a/qt/package/lin/README.md b/qt/bundle/lin/README.md
similarity index 100%
rename from qt/package/lin/README.md
rename to qt/bundle/lin/README.md
diff --git a/qt/package/lin/anki.1 b/qt/bundle/lin/anki.1
similarity index 100%
rename from qt/package/lin/anki.1
rename to qt/bundle/lin/anki.1
diff --git a/qt/package/lin/anki.desktop b/qt/bundle/lin/anki.desktop
similarity index 100%
rename from qt/package/lin/anki.desktop
rename to qt/bundle/lin/anki.desktop
diff --git a/qt/package/lin/anki.png b/qt/bundle/lin/anki.png
similarity index 100%
rename from qt/package/lin/anki.png
rename to qt/bundle/lin/anki.png
diff --git a/qt/package/lin/anki.xml b/qt/bundle/lin/anki.xml
similarity index 100%
rename from qt/package/lin/anki.xml
rename to qt/bundle/lin/anki.xml
diff --git a/qt/package/lin/anki.xpm b/qt/bundle/lin/anki.xpm
similarity index 100%
rename from qt/package/lin/anki.xpm
rename to qt/bundle/lin/anki.xpm
diff --git a/qt/package/lin/install.sh b/qt/bundle/lin/install.sh
similarity index 100%
rename from qt/package/lin/install.sh
rename to qt/bundle/lin/install.sh
diff --git a/qt/package/lin/uninstall.sh b/qt/bundle/lin/uninstall.sh
similarity index 100%
rename from qt/package/lin/uninstall.sh
rename to qt/bundle/lin/uninstall.sh
diff --git a/qt/bundle/mac/Cargo.lock b/qt/bundle/mac/Cargo.lock
new file mode 100644
index 000000000..3d9f6e9c6
--- /dev/null
+++ b/qt/bundle/mac/Cargo.lock
@@ -0,0 +1,204 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "anyhow"
+version = "1.0.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
+
+[[package]]
+name = "apple-bundles"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48681b45ff6789616b243c0758d6d97639951f937ccc0ea635363505d72cdec3"
+dependencies = [
+ "anyhow",
+ "plist",
+ "tugger-file-manifest",
+ "walkdir",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "indexmap"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
+[[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.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0005d08a8f7b65fb8073cb697aa0b12b631ed251ce73d862ce50eeb52ce3b50"
+
+[[package]]
+name = "line-wrap"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
+dependencies = [
+ "safemem",
+]
+
+[[package]]
+name = "makeapp"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "apple-bundles",
+ "glob",
+ "lazy_static",
+ "plist",
+ "tugger-file-manifest",
+ "walkdir",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "plist"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225"
+dependencies = [
+ "base64",
+ "indexmap",
+ "line-wrap",
+ "serde",
+ "time",
+ "xml-rs",
+]
+
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
+[[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 = "serde"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+
+[[package]]
+name = "time"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d"
+dependencies = [
+ "itoa",
+ "libc",
+ "num_threads",
+]
+
+[[package]]
+name = "tugger-file-manifest"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29e91ac69050080a0a9fd50af05da5baa8562347ca7b8909f8ed3adbc6ef026f"
+
+[[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 = "xml-rs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
diff --git a/qt/bundle/mac/Cargo.toml b/qt/bundle/mac/Cargo.toml
new file mode 100644
index 000000000..97355bc25
--- /dev/null
+++ b/qt/bundle/mac/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+edition = "2021"
+name = "makeapp"
+version = "0.1.0"
+authors = ["Ankitects Pty Ltd and contributors"]
+license = "AGPL-3.0-or-later"
+
+[dependencies]
+anyhow = "1.0.53"
+glob = "0.3.0"
+plist = "1.3.1"
+walkdir = "2.3.2"
+apple-bundles= "0.6.0"
+tugger-file-manifest= "0.6.0"
+lazy_static = "1.4.0"
diff --git a/qt/bundle/mac/dmg/anki-logo-bg.png b/qt/bundle/mac/dmg/anki-logo-bg.png
new file mode 100644
index 000000000..84f165653
Binary files /dev/null and b/qt/bundle/mac/dmg/anki-logo-bg.png differ
diff --git a/qt/bundle/mac/dmg/build.sh b/qt/bundle/mac/dmg/build.sh
new file mode 100755
index 000000000..e90b0a814
--- /dev/null
+++ b/qt/bundle/mac/dmg/build.sh
@@ -0,0 +1,42 @@
+# Copyright: Ankitects Pty Ltd and contributors
+# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+set -e
+
+# base folder with Anki.app in it
+dist=$1
+dmg_path=$2
+script_folder=$(dirname $0)
+
+if [ -d "/Volumes/Anki" ]
+then
+ echo "You already have one Anki mounted, unmount it first!"
+ exit 1
+fi
+
+echo "bundling..."
+ln -s /Applications $dist/Applications
+mkdir $dist/.background
+cp ${script_folder}/anki-logo-bg.png $dist/.background
+cp ${script_folder}/dmg_ds_store $dist/.DS_Store
+
+# create a writable dmg first, and modify its layout with AppleScript
+hdiutil create -attach -ov -format UDRW -fs HFS+ -volname Anki -srcfolder $dist -o /tmp/Anki-rw.dmg
+# announce before making the window appear
+say "applescript"
+open /tmp/Anki-rw.dmg
+sleep 2
+open ${script_folder}/set-dmg-settings.app
+sleep 2
+hdiutil detach "/Volumes/Anki"
+sleep 1
+if [ -d "/Volumes/Anki" ]
+then
+ echo "drive did not detach"
+ exit 1
+fi
+
+# convert it to a read-only image
+rm -rf $dmg_path
+hdiutil convert /tmp/Anki-rw.dmg -ov -format ULFO -o $dmg_path
+rm -rf /tmp/Anki-rw.dmg $dist
diff --git a/qt/bundle/mac/dmg/dmg_ds_store b/qt/bundle/mac/dmg/dmg_ds_store
new file mode 100644
index 000000000..632e1a884
Binary files /dev/null and b/qt/bundle/mac/dmg/dmg_ds_store differ
diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Info.plist b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Info.plist
new file mode 100644
index 000000000..068ebc435
--- /dev/null
+++ b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Info.plist
@@ -0,0 +1,74 @@
+
+
+
+
+ CFBundleAllowMixedLocalizations
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ applet
+ CFBundleIconFile
+ applet
+ CFBundleIdentifier
+ com.apple.ScriptEditor.id.set-dmg-settings
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ set-dmg-settings
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ aplt
+ LSMinimumSystemVersionByArchitecture
+
+ x86_64
+ 10.6
+
+ LSRequiresCarbon
+
+ NSAppleEventsUsageDescription
+ This script needs to control other applications to run.
+ NSAppleMusicUsageDescription
+ This script needs access to your music to run.
+ NSCalendarsUsageDescription
+ This script needs access to your calendars to run.
+ NSCameraUsageDescription
+ This script needs access to your camera to run.
+ NSContactsUsageDescription
+ This script needs access to your contacts to run.
+ NSHomeKitUsageDescription
+ This script needs access to your HomeKit Home to run.
+ NSMicrophoneUsageDescription
+ This script needs access to your microphone to run.
+ NSPhotoLibraryUsageDescription
+ This script needs access to your photos to run.
+ NSRemindersUsageDescription
+ This script needs access to your reminders to run.
+ NSSiriUsageDescription
+ This script needs access to Siri to run.
+ NSSystemAdministrationUsageDescription
+ This script needs access to administer this system to run.
+ WindowState
+
+ bundleDividerCollapsed
+
+ bundlePositionOfDivider
+ 0.0
+ dividerCollapsed
+
+ eventLogLevel
+ 2
+ name
+ ScriptWindowState
+ positionOfDivider
+ 388
+ savedFrame
+ 1308 314 700 672 0 0 2880 1597
+ selectedTab
+ result
+
+
+
diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/MacOS/applet b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/MacOS/applet
new file mode 100755
index 000000000..42a481607
Binary files /dev/null and b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/MacOS/applet differ
diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/PkgInfo b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/PkgInfo
new file mode 100644
index 000000000..3253614c4
--- /dev/null
+++ b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/PkgInfo
@@ -0,0 +1 @@
+APPLaplt
\ No newline at end of file
diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/Scripts/main.scpt b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/Scripts/main.scpt
new file mode 100644
index 000000000..7852f0318
Binary files /dev/null and b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/Scripts/main.scpt differ
diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.icns b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.icns
new file mode 100644
index 000000000..0cdd17086
Binary files /dev/null and b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.icns differ
diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.rsrc b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.rsrc
new file mode 100644
index 000000000..868773cbb
Binary files /dev/null and b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.rsrc differ
diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/description.rtfd/TXT.rtf b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/description.rtfd/TXT.rtf
new file mode 100644
index 000000000..b882bf154
--- /dev/null
+++ b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/description.rtfd/TXT.rtf
@@ -0,0 +1,5 @@
+{\rtf1\ansi\ansicpg1252\cocoartf1671
+{\fonttbl}
+{\colortbl;\red255\green255\blue255;}
+{\*\expandedcolortbl;;}
+}
\ No newline at end of file
diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/_CodeSignature/CodeResources b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/_CodeSignature/CodeResources
new file mode 100644
index 000000000..6891d4d4c
--- /dev/null
+++ b/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/_CodeSignature/CodeResources
@@ -0,0 +1,177 @@
+
+
+
+
+ files
+
+ Resources/Scripts/main.scpt
+
+ BbcHsL7M8GleNWeDVHOZVEfpSUQ=
+
+ Resources/applet.icns
+
+ sINd6lbiqHD5dL8c6u79cFvVXhw=
+
+ Resources/applet.rsrc
+
+ 7JOq2AjTwoRdSRoaun87Me8EbB4=
+
+ Resources/description.rtfd/TXT.rtf
+
+ HZLGvORC/avx2snxaACit3D0IJY=
+
+
+ files2
+
+ Resources/Scripts/main.scpt
+
+ hash
+
+ BbcHsL7M8GleNWeDVHOZVEfpSUQ=
+
+ hash2
+
+ T6pvOxUGXyc+qwn+hdv1xPzvnYM+qo9uxLLWUkIFq3Q=
+
+
+ Resources/applet.icns
+
+ hash
+
+ sINd6lbiqHD5dL8c6u79cFvVXhw=
+
+ hash2
+
+ J7weZ6vlnv9r32tS5HFcyuPXl2StdDnfepLxAixlryk=
+
+
+ Resources/applet.rsrc
+
+ hash
+
+ 7JOq2AjTwoRdSRoaun87Me8EbB4=
+
+ hash2
+
+ WvL2TvNeKuY64Sp86Cyvcmiood5xzbJmcAH3R0+gIc8=
+
+
+ Resources/description.rtfd/TXT.rtf
+
+ hash
+
+ HZLGvORC/avx2snxaACit3D0IJY=
+
+ hash2
+
+ XuDTd2OPOPGq65NBuXy6WuqU+bODdg+oDmBFhsZTaVU=
+
+
+
+ rules
+
+ ^Resources/
+
+ ^Resources/.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^Resources/.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Resources/Base\.lproj/
+
+ weight
+ 1010
+
+ ^version.plist$
+
+
+ rules2
+
+ .*\.dSYM($|/)
+
+ weight
+ 11
+
+ ^(.*/)?\.DS_Store$
+
+ omit
+
+ weight
+ 2000
+
+ ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/
+
+ nested
+
+ weight
+ 10
+
+ ^.*
+
+ ^Info\.plist$
+
+ omit
+
+ weight
+ 20
+
+ ^PkgInfo$
+
+ omit
+
+ weight
+ 20
+
+ ^Resources/
+
+ weight
+ 20
+
+ ^Resources/.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^Resources/.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Resources/Base\.lproj/
+
+ weight
+ 1010
+
+ ^[^/]+$
+
+ nested
+
+ weight
+ 10
+
+ ^embedded\.provisionprofile$
+
+ weight
+ 20
+
+ ^version\.plist$
+
+ weight
+ 20
+
+
+
+
diff --git a/qt/bundle/mac/dmg/set-dmg-settings.scpt b/qt/bundle/mac/dmg/set-dmg-settings.scpt
new file mode 100644
index 000000000..ac8d1c2f5
Binary files /dev/null and b/qt/bundle/mac/dmg/set-dmg-settings.scpt differ
diff --git a/qt/bundle/mac/entitlements.python.xml b/qt/bundle/mac/entitlements.python.xml
new file mode 100644
index 000000000..adefed5e4
--- /dev/null
+++ b/qt/bundle/mac/entitlements.python.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.cs.disable-executable-page-protection
+
+ com.apple.security.device.audio-input
+
+ com.apple.security.cs.allow-dyld-environment-variables
+
+ com.apple.security.cs.disable-library-validation
+
+
+
diff --git a/qt/bundle/mac/finalize.py b/qt/bundle/mac/finalize.py
new file mode 100644
index 000000000..d438f1011
--- /dev/null
+++ b/qt/bundle/mac/finalize.py
@@ -0,0 +1,60 @@
+# Copyright: Ankitects Pty Ltd and contributors
+# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+# These steps are outsite bundle/build.py, so that multiple builds can be done
+# in sequence without blocking on Apple's notarization, and so that the final
+# dmg build can be done in bulk at the end.
+
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+output_root = Path(__file__).parent / "../../../.bazel/out"
+dist_folder = output_root / "dist"
+apps = output_root / "build" / "app"
+variants = ["qt6_arm64", "qt6_amd64", "qt5_amd64"]
+
+
+def staple_apps() -> None:
+ for variant in variants:
+ variant_base = apps / variant
+ if variant_base.exists():
+ if os.getenv("NOTARIZE_USER"):
+ subprocess.run(
+ [
+ "python",
+ Path(__file__).with_name("notarize.py"),
+ "staple",
+ variant_base,
+ ],
+ check=True,
+ )
+ else:
+ print("skip missing", variant_base)
+
+
+def build_dmgs() -> None:
+ for variant in variants:
+ variant_base = apps / variant
+ if variant_base.exists():
+ dmg_name_path = variant_base / "dmg_name"
+ dmg_name = open(dmg_name_path).read()
+ dmg_name_path.unlink()
+ subprocess.run(
+ [
+ "bash",
+ Path(__file__).with_name("dmg") / "build.sh",
+ variant_base,
+ dist_folder / dmg_name,
+ ],
+ check=True,
+ )
+ else:
+ print("skip missing", variant_base)
+
+
+if sys.argv[1] == "staple":
+ staple_apps()
+elif sys.argv[1] == "dmg":
+ build_dmgs()
diff --git a/qt/bundle/mac/icon/Assets.car b/qt/bundle/mac/icon/Assets.car
new file mode 100644
index 000000000..d3803495b
Binary files /dev/null and b/qt/bundle/mac/icon/Assets.car differ
diff --git a/qt/bundle/mac/icon/Assets.xcassets/AppIcon.appiconset/Contents.json b/qt/bundle/mac/icon/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..d0600b28d
--- /dev/null
+++ b/qt/bundle/mac/icon/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,59 @@
+{
+ "images" : [
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "16x16"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "16x16"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "32x32"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "32x32"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "128x128"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "128x128"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "256x256"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "256x256"
+ },
+ {
+ "filename" : "round-1024-512.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "512x512"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "512x512"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/qt/bundle/mac/icon/Assets.xcassets/AppIcon.appiconset/round-1024-512.png b/qt/bundle/mac/icon/Assets.xcassets/AppIcon.appiconset/round-1024-512.png
new file mode 100644
index 000000000..3ab3ed40a
Binary files /dev/null and b/qt/bundle/mac/icon/Assets.xcassets/AppIcon.appiconset/round-1024-512.png differ
diff --git a/qt/bundle/mac/icon/Assets.xcassets/Contents.json b/qt/bundle/mac/icon/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/qt/bundle/mac/icon/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/qt/bundle/mac/icon/build.sh b/qt/bundle/mac/icon/build.sh
new file mode 100755
index 000000000..bba481554
--- /dev/null
+++ b/qt/bundle/mac/icon/build.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+set -e
+
+xcrun actool --app-icon AppIcon $(pwd)/Assets.xcassets --compile . --platform macosx --minimum-deployment-target 13.0 --target-device mac --output-partial-info-plist /dev/null
diff --git a/qt/bundle/mac/notarize.py b/qt/bundle/mac/notarize.py
new file mode 100644
index 000000000..acc6d8aae
--- /dev/null
+++ b/qt/bundle/mac/notarize.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python
+# Copyright: Ankitects Pty Ltd and contributors
+# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+import os
+import re
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+USERNAME = os.getenv("NOTARIZE_USER")
+PASSWORD = os.getenv("NOTARIZE_PASSWORD")
+BUNDLE_ID = "net.ankiweb.dtop"
+
+
+def upload(base_dir: Path, uuid_path: Path) -> None:
+ print("--- Prepare notarization zip")
+
+ app_dir = base_dir / "Anki.app"
+ zip_path = app_dir.with_suffix(".zip")
+
+ subprocess.run(["ditto", "-c", "-k", "--keepParent", app_dir, zip_path])
+
+ print("--- Upload for notarization")
+
+ try:
+ output = subprocess.check_output(
+ [
+ "xcrun",
+ "altool",
+ "--notarize-app",
+ "--primary-bundle-id",
+ BUNDLE_ID,
+ "--username",
+ USERNAME,
+ "--password",
+ PASSWORD,
+ "--file",
+ zip_path,
+ ],
+ stderr=subprocess.STDOUT,
+ encoding="utf8",
+ )
+ except subprocess.CalledProcessError as e:
+ print("error uploading:", e.output)
+ sys.exit(1)
+
+ uuid = None
+ for line in output.splitlines():
+ m = re.search(r"RequestUUID = (.*)", line)
+ if m:
+ uuid = m.group(1)
+
+ if not uuid:
+ print("no uuid found - upload output:")
+ print(output)
+ sys.exit(1)
+
+ open(uuid_path, "w").write(uuid)
+ zip_path.unlink()
+
+
+def _extract_status(output):
+ for line in output.splitlines():
+ m = re.search(r"Status: (.*)", line)
+ if m:
+ return m.group(1)
+
+ return None
+
+
+def wait_for_success(uuid, wait=True):
+ while True:
+ print("checking status...", end="", flush=True)
+
+ try:
+ output = subprocess.check_output(
+ [
+ "xcrun",
+ "altool",
+ "--notarization-info",
+ uuid,
+ "--username",
+ USERNAME,
+ "--password",
+ PASSWORD,
+ ],
+ stderr=subprocess.STDOUT,
+ encoding="utf8",
+ )
+ except subprocess.CalledProcessError as e:
+ print("error checking status:")
+ print(e.output)
+ sys.exit(1)
+
+ status = _extract_status(output)
+ if status is None:
+ print("could not extract status:")
+ print(output)
+ sys.exit(1)
+
+ if status == "invalid":
+ print("notarization failed:")
+ print(output)
+ sys.exit(1)
+
+ if status == "success":
+ print("success!")
+ print(output)
+ return
+
+ print(status)
+ if not wait:
+ return
+ time.sleep(30)
+
+
+def staple(app_path):
+ try:
+ subprocess.check_call(
+ [
+ "xcrun",
+ "stapler",
+ "staple",
+ app_path,
+ ]
+ )
+ except subprocess.CalledProcessError as e:
+ print("error stapling:")
+ print(e.output)
+ sys.exit(1)
+
+
+cmd = sys.argv[1]
+base_dir = Path(sys.argv[2])
+uuid_path = base_dir / "uuid"
+
+if cmd == "upload":
+ upload(base_dir, uuid_path)
+elif cmd == "status":
+ uuid = open(uuid_path).read()
+ wait_for_success(uuid, False)
+elif cmd == "staple":
+ uuid = open(uuid_path).read()
+ wait_for_success(uuid)
+ staple(base_dir / "Anki.app")
+ uuid_path.unlink()
diff --git a/qt/bundle/mac/src/Info.plist b/qt/bundle/mac/src/Info.plist
new file mode 100644
index 000000000..b960057cd
--- /dev/null
+++ b/qt/bundle/mac/src/Info.plist
@@ -0,0 +1,49 @@
+
+
+
+
+ CFBundleDisplayName
+ Anki
+ CFBundleShortVersionString
+ 2.1.46
+ LSMinimumSystemVersion
+ 10.13.4
+ CFBundleDocumentTypes
+
+
+ CFBundleTypeExtensions
+
+ colpkg
+ apkg
+ ankiaddon
+
+ CFBundleTypeIconName
+ AppIcon
+ CFBundleTypeName
+ Anki File
+ CFBundleTypeRole
+ Editor
+
+
+ CFBundleExecutable
+ MacOS/anki
+ CFBundleIconName
+ AppIcon
+ CFBundleIdentifier
+ net.ankiweb.dtop
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ Anki
+ CFBundlePackageType
+ APPL
+ NSHighResolutionCapable
+
+ NSMicrophoneUsageDescription
+ The microphone will only be used when you tap the record button.
+ NSRequiresAquaSystemAppearance
+
+ NSSupportsAutomaticGraphicsSwitching
+
+
+
diff --git a/qt/bundle/mac/src/main.rs b/qt/bundle/mac/src/main.rs
new file mode 100644
index 000000000..dd77271bf
--- /dev/null
+++ b/qt/bundle/mac/src/main.rs
@@ -0,0 +1,223 @@
+// Copyright: Ankitects Pty Ltd and contributors
+// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+//! Munge the output of PyOxidizer into a macOS app bundle, and combine it
+//! with our other runtime dependencies.
+
+use std::{
+ path::{Path, PathBuf},
+ str::FromStr,
+};
+
+use anyhow::{bail, Context, Result};
+use apple_bundles::MacOsApplicationBundleBuilder;
+use plist::Value;
+use tugger_file_manifest::FileEntry;
+use walkdir::WalkDir;
+
+const CODESIGN_ARGS: &[&str] = &["-vvvv", "-o", "runtime", "-s", "Developer ID Application:"];
+
+#[derive(Clone, Copy, Debug)]
+enum Variant {
+ StandardX86,
+ StandardArm,
+ AlternateX86,
+}
+
+impl FromStr for Variant {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result {
+ Ok(match s {
+ "qt6_arm64" => Variant::StandardArm,
+ "qt6_amd64" => Variant::StandardX86,
+ "qt5_amd64" => Variant::AlternateX86,
+ other => bail!("unexpected variant: {other}"),
+ })
+ }
+}
+
+impl Variant {
+ fn output_base(&self) -> &str {
+ match self {
+ Variant::StandardX86 => "qt6_amd64",
+ Variant::StandardArm => "qt6_arm64",
+ Variant::AlternateX86 => "qt5_amd64",
+ }
+ }
+
+ fn macos_min(&self) -> &str {
+ match self {
+ Variant::StandardX86 => "10.14.4",
+ Variant::StandardArm => "11",
+ Variant::AlternateX86 => "10.13.4",
+ }
+ }
+
+ fn qt_repo(&self) -> &str {
+ match self {
+ Variant::StandardX86 => "pyqt6.2_mac_bundle_amd64",
+ Variant::StandardArm => "pyqt6.2_mac_bundle_arm64",
+ Variant::AlternateX86 => "pyqt5.14_mac_bundle_amd64",
+ }
+ }
+
+ fn audio_repo(&self) -> &str {
+ match self {
+ Variant::StandardX86 | Variant::AlternateX86 => "audio_mac_amd64",
+ Variant::StandardArm => "audio_mac_arm64",
+ }
+ }
+}
+
+fn main() -> anyhow::Result<()> {
+ let args: Vec<_> = std::env::args().collect();
+ let variant: Variant = args.get(1).context("variant")?.parse()?;
+ let bundle_folder = PathBuf::from(args.get(2).context("bundle folder")?);
+ let anki_version = args.get(3).context("anki version")?;
+ let bazel_external = PathBuf::from(args.get(4).context("bazel external folder")?);
+
+ let plist = get_plist(anki_version);
+ make_app(variant, &bundle_folder, plist, &bazel_external)
+}
+
+fn make_app(
+ variant: Variant,
+ input_folder: &Path,
+ mut plist: plist::Dictionary,
+ bazel_external: &Path,
+) -> Result<()> {
+ let output_folder = input_folder
+ .with_file_name("app")
+ .join(variant.output_base())
+ .join("Anki.app");
+ if output_folder.exists() {
+ std::fs::remove_dir_all(&output_folder)?;
+ }
+ std::fs::create_dir_all(&output_folder)?;
+
+ let mut builder = MacOsApplicationBundleBuilder::new("Anki")?;
+ plist.insert(
+ "LSMinimumSystemVersion".into(),
+ Value::from(variant.macos_min()),
+ );
+ builder.set_info_plist_from_dictionary(plist)?;
+ builder.add_file_resources("Assets.car", &include_bytes!("../icon/Assets.car")[..])?;
+
+ for entry in WalkDir::new(&input_folder)
+ .into_iter()
+ .map(Result::unwrap)
+ .filter(|e| !e.file_type().is_dir())
+ {
+ let path = entry.path();
+ let entry = FileEntry::try_from(path)?;
+ let relative_path = path.strip_prefix(&input_folder)?;
+ let path_str = relative_path.to_str().unwrap();
+ if path_str.contains("libankihelper") {
+ builder.add_file_macos("libankihelper.dylib", entry)?;
+ } else if path_str.contains("aqt/data") {
+ builder.add_file_resources(relative_path.strip_prefix("lib").unwrap(), entry)?;
+ } else {
+ if path_str.contains("__pycache__") {
+ continue;
+ }
+ builder.add_file_macos(relative_path, entry)?;
+ }
+ }
+
+ let dry_run = false;
+ if dry_run {
+ for file in builder.files().iter_files() {
+ println!("{}", file.path_string());
+ }
+ } else {
+ builder.files().materialize_files(&output_folder)?;
+ fix_rpath(output_folder.join("Contents/MacOS/anki"))?;
+ codesign_python_libs(&output_folder)?;
+ copy_in_audio(&output_folder, variant, bazel_external)?;
+ copy_in_qt(&output_folder, variant, bazel_external)?;
+ codesign_app(&output_folder)?;
+ }
+
+ Ok(())
+}
+
+/// Copy everything at the provided path into the Contents/ folder of our app.
+/// Excludes standard Bazel repo files.
+fn extend_app_contents(source: &Path, bundle_dir: &Path) -> Result<()> {
+ let status = std::process::Command::new("rsync")
+ .arg("-a")
+ .args(["--exclude", "BUILD.bazel", "--exclude", "WORKSPACE"])
+ .arg(format!("{}/", source.to_string_lossy()))
+ .arg(bundle_dir.join("Contents/"))
+ .status()?;
+ if !status.success() {
+ bail!("error syncing {source:?}");
+ }
+ Ok(())
+}
+
+fn copy_in_audio(bundle_dir: &Path, variant: Variant, bazel_external: &Path) -> Result<()> {
+ println!("Copying in audio...");
+ extend_app_contents(&bazel_external.join(variant.audio_repo()), bundle_dir)
+}
+
+fn copy_in_qt(bundle_dir: &Path, variant: Variant, bazel_external: &Path) -> Result<()> {
+ println!("Copying in Qt...");
+ extend_app_contents(&bazel_external.join(variant.qt_repo()), bundle_dir)
+}
+
+fn codesign_file(path: &Path, extra_args: &[&str]) -> Result<()> {
+ if option_env!("ANKI_CODESIGN").is_some() {
+ let status = std::process::Command::new("codesign")
+ .args(CODESIGN_ARGS)
+ .args(extra_args)
+ .arg(path.to_str().unwrap())
+ .status()?;
+ if !status.success() {
+ bail!("codesign failed");
+ }
+ }
+
+ Ok(())
+}
+
+fn codesign_python_libs(bundle_dir: &PathBuf) -> Result<()> {
+ for entry in glob::glob(
+ bundle_dir
+ .join("Contents/MacOS/lib/**/*.so")
+ .to_str()
+ .unwrap(),
+ )? {
+ let entry = entry?;
+ codesign_file(&entry, &[])?;
+ }
+ codesign_file(&bundle_dir.join("Contents/MacOS/libankihelper.dylib"), &[])
+}
+
+fn codesign_app(bundle_dir: &PathBuf) -> Result<()> {
+ codesign_file(bundle_dir, &["--entitlements", "entitlements.python.xml"])
+}
+
+fn fix_rpath(exe_path: PathBuf) -> Result<()> {
+ let status = std::process::Command::new("install_name_tool")
+ .arg("-add_rpath")
+ .arg("@executable_path/../Frameworks")
+ .arg(exe_path.to_str().unwrap())
+ .status()?;
+ assert!(status.success());
+ Ok(())
+}
+
+fn get_plist(anki_version: &str) -> plist::Dictionary {
+ let reader = std::io::Cursor::new(include_bytes!("Info.plist"));
+ let mut plist = plist::Value::from_reader(reader)
+ .unwrap()
+ .into_dictionary()
+ .unwrap();
+ plist.insert(
+ "CFBundleShortVersionString".into(),
+ Value::from(anki_version),
+ );
+ plist
+}
diff --git a/qt/package/pyoxidizer.bzl b/qt/bundle/pyoxidizer.bzl
similarity index 96%
rename from qt/package/pyoxidizer.bzl
rename to qt/bundle/pyoxidizer.bzl
index c70052272..be78a1ad8 100644
--- a/qt/package/pyoxidizer.bzl
+++ b/qt/bundle/pyoxidizer.bzl
@@ -1,4 +1,4 @@
-set_build_path("../../bazel-pkg/build")
+set_build_path(VARS.get("build"))
excluded_source_prefixes = [
"ctypes.test",
@@ -131,6 +131,9 @@ def make_exe():
# detected libs do not need this, but we add extra afterwards
python_config.module_search_paths = ["$ORIGIN/lib"]
python_config.optimization_level = 2
+ if BUILD_TARGET_TRIPLE == "x86_64-apple-darwin":
+ # jemalloc currently fails to build when run under Rosetta
+ python_config.allocator_backend = "default"
python_config.run_command = "import aqt; aqt.run()"
diff --git a/qt/package/qt.exclude b/qt/bundle/qt.exclude
similarity index 100%
rename from qt/package/qt.exclude
rename to qt/bundle/qt.exclude
diff --git a/qt/package/rustfmt.toml b/qt/bundle/rustfmt.toml
similarity index 100%
rename from qt/package/rustfmt.toml
rename to qt/bundle/rustfmt.toml
diff --git a/qt/package/src/anki.rs b/qt/bundle/src/anki.rs
similarity index 100%
rename from qt/package/src/anki.rs
rename to qt/bundle/src/anki.rs
diff --git a/qt/package/src/main.rs b/qt/bundle/src/main.rs
similarity index 100%
rename from qt/package/src/main.rs
rename to qt/bundle/src/main.rs
diff --git a/qt/bundle/win/Cargo.lock b/qt/bundle/win/Cargo.lock
new file mode 100644
index 000000000..138492420
--- /dev/null
+++ b/qt/bundle/win/Cargo.lock
@@ -0,0 +1,1497 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "anyhow"
+version = "1.0.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[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 = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-modes"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e"
+dependencies = [
+ "block-padding",
+ "cipher",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
+
+[[package]]
+name = "bumpalo"
+version = "3.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
+[[package]]
+name = "bzip2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0"
+dependencies = [
+ "bzip2-sys",
+ "libc",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.11+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "time",
+ "winapi",
+]
+
+[[package]]
+name = "cipher"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-mac"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "des"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac41dd49fb554432020d52c875fc290e110113f864c6b1b525cd62c7e7747a5d"
+dependencies = [
+ "byteorder",
+ "cipher",
+ "opaque-debug",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "dirs-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "duct"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc6a0a59ed0888e0041cf708e66357b7ae1a82f1c67247e1f93b5e0818f7d8d"
+dependencies = [
+ "libc",
+ "once_cell",
+ "os_pipe",
+ "shared_child",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "find-winsdk"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8cbf17b871570c1f8612b763bac3e86290602bcf5dc3c5ce657e0e1e9071d9e"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "winreg 0.5.1",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
+dependencies = [
+ "cfg-if",
+ "crc32fast",
+ "libc",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
+
+[[package]]
+name = "futures-io"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
+
+[[package]]
+name = "futures-task"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
+
+[[package]]
+name = "futures-util"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
+[[package]]
+name = "h2"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
+dependencies = [
+ "crypto-mac",
+ "digest",
+]
+
+[[package]]
+name = "http"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa 1.0.1",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
+name = "hyper"
+version = "0.14.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa 0.4.8",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac"
+dependencies = [
+ "http",
+ "hyper",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
+[[package]]
+name = "js-sys"
+version = "0.3.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[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.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c"
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "makeinstall"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "glob",
+ "slog",
+ "slog-term",
+ "tugger-windows-codesign",
+ "walkdir",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
+dependencies = [
+ "adler",
+ "autocfg",
+]
+
+[[package]]
+name = "mio"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "os_pipe"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "p12"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10105b08ad3c4cd872ddf396860544f9dbc1800fed7d552f10aa5b585ac79e1d"
+dependencies = [
+ "block-modes",
+ "des",
+ "getrandom",
+ "hmac",
+ "lazy_static",
+ "rc2",
+ "sha-1",
+ "yasna",
+]
+
+[[package]]
+name = "pem"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947"
+dependencies = [
+ "base64",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rc2"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48f197c283075d1345c20d5ad172526a7837882cdc998b1fcd2b2f3cfff1cb94"
+dependencies = [
+ "cipher",
+ "opaque-debug",
+]
+
+[[package]]
+name = "rcgen"
+version = "0.8.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5911d1403f4143c9d56a702069d593e8d0f3fab880a85e103604d0893ea31ba7"
+dependencies = [
+ "chrono",
+ "pem",
+ "ring",
+ "yasna",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+]
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-rustls",
+ "ipnet",
+ "js-sys",
+ "lazy_static",
+ "log",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-rustls",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+ "winreg 0.7.0",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "rustls"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84"
+dependencies = [
+ "log",
+ "ring",
+ "sct",
+ "webpki",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9"
+dependencies = [
+ "base64",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f"
+
+[[package]]
+name = "ryu"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
+
+[[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 = "sct"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7"
+
+[[package]]
+name = "serde"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085"
+dependencies = [
+ "itoa 1.0.1",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa 1.0.1",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha-1"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
+dependencies = [
+ "block-buffer",
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+dependencies = [
+ "block-buffer",
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "shared_child"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6be9f7d5565b1483af3e72975e2dee33879b3b86bd48c0929fccf6585d79e65a"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
+
+[[package]]
+name = "slog"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06"
+
+[[package]]
+name = "slog-term"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95c1e7e5aab61ced6006149ea772770b84a0d16ce0f7885def313e4829946d76"
+dependencies = [
+ "atty",
+ "chrono",
+ "slog",
+ "term",
+ "thread_local",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
+[[package]]
+name = "syn"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "term"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
+dependencies = [
+ "dirs-next",
+ "rustversion",
+ "winapi",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a"
+dependencies = [
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b"
+dependencies = [
+ "rustls",
+ "tokio",
+ "webpki",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+
+[[package]]
+name = "tracing"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "tugger-common"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a7438a6e6ed337853dfeb03e3eb463c25d60141186b65363a1dfa3ed8fa6bab"
+dependencies = [
+ "anyhow",
+ "fs2",
+ "glob",
+ "hex",
+ "once_cell",
+ "reqwest",
+ "sha2",
+ "slog",
+ "tempfile",
+ "url",
+ "zip",
+]
+
+[[package]]
+name = "tugger-windows"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "862f31d91b88b479a2671584a9772f81ec3d21b8314adc4fdf17619b12f8750c"
+dependencies = [
+ "anyhow",
+ "duct",
+ "find-winsdk",
+ "glob",
+ "once_cell",
+ "semver",
+ "tugger-common",
+ "winapi",
+]
+
+[[package]]
+name = "tugger-windows-codesign"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99fe75b7e13b3ba9d95001be2f5a0b5bd1246d38492f5658fd5e90c1006eed00"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "duct",
+ "p12",
+ "rcgen",
+ "slog",
+ "tugger-common",
+ "tugger-windows",
+ "yasna",
+]
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[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 = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
+
+[[package]]
+name = "web-sys"
+version = "0.3.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.22.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449"
+dependencies = [
+ "webpki",
+]
+
+[[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.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a"
+dependencies = [
+ "serde",
+ "winapi",
+]
+
+[[package]]
+name = "winreg"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "yasna"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e262a29d0e61ccf2b6190d7050d4b237535fc76ce4c1210d9caa316f71dffa75"
+dependencies = [
+ "chrono",
+]
+
+[[package]]
+name = "zip"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815"
+dependencies = [
+ "byteorder",
+ "bzip2",
+ "crc32fast",
+ "flate2",
+ "thiserror",
+ "time",
+]
diff --git a/qt/bundle/win/Cargo.toml b/qt/bundle/win/Cargo.toml
new file mode 100644
index 000000000..757a3ad29
--- /dev/null
+++ b/qt/bundle/win/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+edition = "2021"
+name = "makeinstall"
+version = "0.1.0"
+authors = ["Ankitects Pty Ltd and contributors"]
+license = "AGPL-3.0-or-later"
+
+[dependencies]
+anyhow = "1.0.53"
+glob = "0.3.0"
+slog = "2.7.0"
+slog-term = "2.8.0"
+tugger-windows-codesign = "0.6.0"
+walkdir = "2.3.2"
diff --git a/qt/package/win/anki-console.bat b/qt/bundle/win/anki-console.bat
similarity index 100%
rename from qt/package/win/anki-console.bat
rename to qt/bundle/win/anki-console.bat
diff --git a/qt/package/anki-icon.ico b/qt/bundle/win/anki-icon.ico
similarity index 100%
rename from qt/package/anki-icon.ico
rename to qt/bundle/win/anki-icon.ico
diff --git a/qt/package/anki-manifest.rc b/qt/bundle/win/anki-manifest.rc
similarity index 100%
rename from qt/package/anki-manifest.rc
rename to qt/bundle/win/anki-manifest.rc
diff --git a/qt/package/anki.exe.manifest b/qt/bundle/win/anki.exe.manifest
similarity index 100%
rename from qt/package/anki.exe.manifest
rename to qt/bundle/win/anki.exe.manifest
diff --git a/qt/bundle/win/anki.template.nsi b/qt/bundle/win/anki.template.nsi
new file mode 100644
index 000000000..db414b943
--- /dev/null
+++ b/qt/bundle/win/anki.template.nsi
@@ -0,0 +1,210 @@
+;; This installer was written many years ago, and it is probably worth investigating modern
+;; installer alternatives.
+
+!include "fileassoc.nsh"
+!include WinVer.nsh
+!include x64.nsh
+; must be installed into NSIS install location
+; can be found on https://github.com/ankitects/anki-bundle-extras/releases/tag/anki-2022-02-09
+!include nsProcess.nsh
+
+;--------------------------------
+
+!pragma warning disable 6020 ; don't complain about missing installer in second invocation
+
+; The name of the installer
+Name "Anki"
+
+Unicode true
+
+; The file to write
+OutFile "anki-setup.exe"
+
+; The default installation directory
+InstallDir "$PROGRAMFILES64\Anki"
+
+; Remember the install location
+InstallDirRegKey HKLM "Software\Anki" "Install_Dir64"
+
+AllowSkipFiles off
+
+!ifdef NO_COMPRESS
+SetCompress off
+!else
+SetCompressor /solid lzma
+!endif
+
+Function .onInit
+ ${IfNot} ${AtLeastWin10}
+ MessageBox MB_OK "Windows 10 or later required."
+ Quit
+ ${EndIf}
+
+ ${IfNot} ${RunningX64}
+ MessageBox MB_OK "64bit Windows is required."
+ Quit
+ ${EndIf}
+
+ ${nsProcess::FindProcess} "anki.exe" $R0
+ StrCmp $R0 0 0 notRunning
+ MessageBox MB_OK|MB_ICONEXCLAMATION "Anki.exe is already running. Please close it, then run the installer again." /SD IDOK
+ Abort
+ notRunning:
+FunctionEnd
+
+!ifdef WRITE_UNINSTALLER
+!uninstfinalize 'copy "%1" "std\uninstall.exe"'
+!endif
+
+;--------------------------------
+
+; Pages
+
+Page directory
+Page instfiles
+
+
+;; manifest removal script shared by installer and uninstaller
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+!define UninstLog "anki.install-manifest"
+Var UninstLog
+
+!macro removeManifestFiles un
+Function ${un}removeManifestFiles
+ IfFileExists "$INSTDIR\${UninstLog}" proceed
+ DetailPrint "No previous install manifest found, skipping cleanup."
+ return
+
+;; this code was based on an example found on the net, which I can no longer find
+proceed:
+ Push $R0
+ Push $R1
+ Push $R2
+ SetFileAttributes "$INSTDIR\${UninstLog}" NORMAL
+ FileOpen $UninstLog "$INSTDIR\${UninstLog}" r
+ StrCpy $R1 -1
+
+ GetLineCount:
+ ClearErrors
+ FileRead $UninstLog $R0
+ IntOp $R1 $R1 + 1
+ StrCpy $R0 $R0 -2
+ Push $R0
+ IfErrors 0 GetLineCount
+
+ Pop $R0
+
+ LoopRead:
+ StrCmp $R1 0 LoopDone
+ Pop $R0
+ ;; manifest is relative to instdir
+ StrCpy $R0 "$INSTDIR\$R0"
+
+ IfFileExists "$R0\*.*" 0 +3
+ RMDir $R0 #is dir
+ Goto processed
+ IfFileExists $R0 0 +3
+ Delete $R0 #is file
+ Goto processed
+
+processed:
+
+ IntOp $R1 $R1 - 1
+ Goto LoopRead
+ LoopDone:
+ FileClose $UninstLog
+ Delete "$INSTDIR\${UninstLog}"
+ RMDir "$INSTDIR"
+ Pop $R2
+ Pop $R1
+ Pop $R0
+FunctionEnd
+!macroend
+
+!insertmacro removeManifestFiles ""
+!insertmacro removeManifestFiles "un."
+
+;--------------------------------
+
+; The stuff to install
+Section ""
+
+ SetShellVarContext all
+
+ Call removeManifestFiles
+
+ ; Set output path to the installation directory.
+ SetOutPath $INSTDIR
+ CreateShortCut "$DESKTOP\Anki.lnk" "$INSTDIR\anki.exe" ""
+ CreateShortCut "$SMPROGRAMS\Anki.lnk" "$INSTDIR\anki.exe" ""
+
+ ; Add files to installer
+ !ifndef WRITE_UNINSTALLER
+ File /r @@SRC@@\*.*
+ !endif
+
+ !insertmacro APP_ASSOCIATE "apkg" "anki.apkg" \
+ "Anki deck package" "$INSTDIR\anki.exe,0" \
+ "Open with Anki" "$INSTDIR\anki.exe $\"%L$\""
+
+ !insertmacro APP_ASSOCIATE "colpkg" "anki.colpkg" \
+ "Anki collection package" "$INSTDIR\anki.exe,0" \
+ "Open with Anki" "$INSTDIR\anki.exe $\"%L$\""
+
+ !insertmacro APP_ASSOCIATE "ankiaddon" "anki.ankiaddon" \
+ "Anki add-on" "$INSTDIR\anki.exe,0" \
+ "Open with Anki" "$INSTDIR\anki.exe $\"%L$\""
+
+ !insertmacro UPDATEFILEASSOC
+
+ ; Write the installation path into the registry
+ WriteRegStr HKLM Software\Anki "Install_Dir64" "$INSTDIR"
+
+ ; Write the uninstall keys for Windows
+ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayName" "Anki"
+ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayVersion" "@@VERSION@@"
+ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "UninstallString" '"$INSTDIR\uninstall.exe"'
+ WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "NoModify" 1
+ WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "NoRepair" 1
+
+ !ifdef WRITE_UNINSTALLER
+ WriteUninstaller "uninstall.exe"
+ !endif
+
+SectionEnd ; end the section
+
+;--------------------------------
+
+; Uninstaller
+
+function un.onInit
+ MessageBox MB_OKCANCEL "This will remove Anki's program files, but will not delete your card data. If you wish to delete your card data as well, you can do so via File>Switch Profile inside Anki first. Are you sure you wish to uninstall Anki?" IDOK next
+ Quit
+ next:
+functionEnd
+
+Section "Uninstall"
+
+ SetShellVarContext all
+
+ Call un.removeManifestFiles
+
+ ; Remove other shortcuts
+ Delete "$DESKTOP\Anki.lnk"
+ Delete "$SMPROGRAMS\Anki.lnk"
+
+ ; associations
+ !insertmacro APP_UNASSOCIATE "apkg" "anki.apkg"
+ !insertmacro APP_UNASSOCIATE "colpkg" "anki.colpkg"
+ !insertmacro APP_UNASSOCIATE "ankiaddon" "anki.ankiaddon"
+ !insertmacro UPDATEFILEASSOC
+
+ ; try to remove top level folder if empty
+ RMDir "$INSTDIR"
+
+ ; Remove registry keys
+ DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki"
+ DeleteRegKey HKLM Software\Anki
+
+SectionEnd
diff --git a/qt/bundle/win/fileassoc.nsh b/qt/bundle/win/fileassoc.nsh
new file mode 100644
index 000000000..f76c28e35
--- /dev/null
+++ b/qt/bundle/win/fileassoc.nsh
@@ -0,0 +1,120 @@
+; fileassoc.nsh
+; https://nsis.sourceforge.io/File_Association
+; File association helper macros
+; Written by Saivert
+;
+; Features automatic backup system and UPDATEFILEASSOC macro for
+; shell change notification.
+;
+; |> How to use <|
+; To associate a file with an application so you can double-click it in explorer, use
+; the APP_ASSOCIATE macro like this:
+;
+; Example:
+; !insertmacro APP_ASSOCIATE "txt" "myapp.textfile" "$INSTDIR\myapp.exe,0" \
+; "Open with myapp" "$INSTDIR\myapp.exe $\"%1$\""
+;
+; Never insert the APP_ASSOCIATE macro multiple times, it is only ment
+; to associate an application with a single file and using the
+; the "open" verb as default. To add more verbs (actions) to a file
+; use the APP_ASSOCIATE_ADDVERB macro.
+;
+; Example:
+; !insertmacro APP_ASSOCIATE_ADDVERB "myapp.textfile" "edit" "Edit with myapp" \
+; "$INSTDIR\myapp.exe /edit $\"%1$\""
+;
+; To have access to more options when registering the file association use the
+; APP_ASSOCIATE_EX macro. Here you can specify the verb and what verb is to be the
+; standard action (default verb).
+;
+; And finally: To remove the association from the registry use the APP_UNASSOCIATE
+; macro. Here is another example just to wrap it up:
+; !insertmacro APP_UNASSOCIATE "txt" "myapp.textfile"
+;
+; |> Note <|
+; When defining your file class string always use the short form of your application title
+; then a period (dot) and the type of file. This keeps the file class sort of unique.
+; Examples:
+; Winamp.Playlist
+; NSIS.Script
+; Photoshop.JPEGFile
+;
+; |> Tech info <|
+; The registry key layout for a file association is:
+; HKEY_CLASSES_ROOT
+; = <"description">
+; shell
+; = <"menu-item text">
+; command = <"command string">
+;
+
+!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
+ ; Backup the previously associated file class
+ ReadRegStr $R0 HKCR ".${EXT}" ""
+ WriteRegStr HKCR ".${EXT}" "${FILECLASS}_backup" "$R0"
+
+ WriteRegStr HKCR ".${EXT}" "" "${FILECLASS}"
+
+ WriteRegStr HKCR "${FILECLASS}" "" `${DESCRIPTION}`
+ WriteRegStr HKCR "${FILECLASS}\DefaultIcon" "" `${ICON}`
+ WriteRegStr HKCR "${FILECLASS}\shell" "" "open"
+ WriteRegStr HKCR "${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
+ WriteRegStr HKCR "${FILECLASS}\shell\open\command" "" `${COMMAND}`
+!macroend
+
+!macro APP_ASSOCIATE_EX EXT FILECLASS DESCRIPTION ICON VERB DEFAULTVERB SHELLNEW COMMANDTEXT COMMAND
+ ; Backup the previously associated file class
+ ReadRegStr $R0 HKCR ".${EXT}" ""
+ WriteRegStr HKCR ".${EXT}" "${FILECLASS}_backup" "$R0"
+
+ WriteRegStr HKCR ".${EXT}" "" "${FILECLASS}"
+ StrCmp "${SHELLNEW}" "0" +2
+ WriteRegStr HKCR ".${EXT}\ShellNew" "NullFile" ""
+
+ WriteRegStr HKCR "${FILECLASS}" "" `${DESCRIPTION}`
+ WriteRegStr HKCR "${FILECLASS}\DefaultIcon" "" `${ICON}`
+ WriteRegStr HKCR "${FILECLASS}\shell" "" `${DEFAULTVERB}`
+ WriteRegStr HKCR "${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}`
+ WriteRegStr HKCR "${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}`
+!macroend
+
+!macro APP_ASSOCIATE_ADDVERB FILECLASS VERB COMMANDTEXT COMMAND
+ WriteRegStr HKCR "${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}`
+ WriteRegStr HKCR "${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}`
+!macroend
+
+!macro APP_ASSOCIATE_REMOVEVERB FILECLASS VERB
+ DeleteRegKey HKCR `${FILECLASS}\shell\${VERB}`
+!macroend
+
+
+!macro APP_UNASSOCIATE EXT FILECLASS
+ ; Backup the previously associated file class
+ ReadRegStr $R0 HKCR ".${EXT}" `${FILECLASS}_backup`
+ WriteRegStr HKCR ".${EXT}" "" "$R0"
+
+ DeleteRegKey HKCR `${FILECLASS}`
+!macroend
+
+!macro APP_ASSOCIATE_GETFILECLASS OUTPUT EXT
+ ReadRegStr ${OUTPUT} HKCR ".${EXT}" ""
+!macroend
+
+
+; !defines for use with SHChangeNotify
+!ifdef SHCNE_ASSOCCHANGED
+!undef SHCNE_ASSOCCHANGED
+!endif
+!define SHCNE_ASSOCCHANGED 0x08000000
+!ifdef SHCNF_FLUSH
+!undef SHCNF_FLUSH
+!endif
+!define SHCNF_FLUSH 0x1000
+
+!macro UPDATEFILEASSOC
+; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we
+; can update the shell.
+ System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)"
+!macroend
+
+;EOF
diff --git a/qt/bundle/win/rustfmt.toml b/qt/bundle/win/rustfmt.toml
new file mode 100644
index 000000000..3c812a2b9
--- /dev/null
+++ b/qt/bundle/win/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/bundle/win/src/main.rs b/qt/bundle/win/src/main.rs
new file mode 100644
index 000000000..0a866be56
--- /dev/null
+++ b/qt/bundle/win/src/main.rs
@@ -0,0 +1,222 @@
+// Copyright: Ankitects Pty Ltd and contributors
+// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
+use std::{
+ fs,
+ io::prelude::*,
+ path::{Component, Path, PathBuf, Prefix},
+ process::Command,
+};
+
+use anyhow::{bail, Context, Result};
+use slog::*;
+use tugger_windows_codesign::{CodeSigningCertificate, SigntoolSign, SystemStore, TimestampServer};
+use walkdir::WalkDir;
+
+fn main() -> anyhow::Result<()> {
+ let plain = slog_term::PlainSyncDecorator::new(std::io::stdout());
+ let logger = Logger::root(slog_term::FullFormat::new(plain).build().fuse(), o!());
+
+ let args: Vec<_> = std::env::args().collect();
+ let build_folder = PathBuf::from(args.get(1).context("build folder")?);
+ let bazel_external = PathBuf::from(args.get(2).context("bazel external")?);
+ // bundle/build.py folder
+ let build_py_folder = PathBuf::from(args.get(3).context("build_py_folder")?);
+ let version = args.get(4).context("version")?;
+
+ let std_folder = build_folder.join("std");
+ let alt_folder = build_folder.join("alt");
+ let folders = &[&std_folder, &alt_folder];
+
+ for folder in folders {
+ fs::copy(
+ build_py_folder.join("win").join("anki-console.bat"),
+ folder.join("anki-console.bat"),
+ )
+ .context("anki-console")?;
+ }
+
+ println!("--- Copy in audio");
+ copy_in_audio(&std_folder, &bazel_external)?;
+ copy_in_audio(&alt_folder, &bazel_external)?;
+
+ println!("--- Build uninstaller");
+ build_installer(&std_folder, &build_folder, version, true).context("uninstaller")?;
+
+ // sign the anki.exe and uninstaller.exe in std, then copy into alt
+ println!("--- Sign binaries");
+ codesign(
+ &logger,
+ &[
+ &std_folder.join("anki.exe"),
+ &std_folder.join("uninstall.exe"),
+ ],
+ )?;
+ for fname in &["anki.exe", "uninstall.exe"] {
+ fs::copy(std_folder.join(fname), alt_folder.join(fname))
+ .with_context(|| format!("copy {fname}"))?;
+ }
+
+ println!("--- Build manifest");
+ for folder in folders {
+ build_manifest(folder).context("manifest")?;
+ }
+
+ let mut installer_paths = vec![];
+ for (folder, variant) in folders.iter().zip(&["qt6", "qt5"]) {
+ println!(
+ "--- Build installer for {}",
+ folder.file_name().unwrap().to_str().unwrap()
+ );
+ build_installer(folder, &build_folder, version, false)?;
+ let installer_filename = format!("anki-{version}-windows-{variant}.exe");
+ let installer_path = build_folder
+ .join("..")
+ .join("dist")
+ .join(installer_filename);
+
+ fs::rename(build_folder.join("anki-setup.exe"), &installer_path)
+ .context("rename installer")?;
+ installer_paths.push(installer_path);
+ }
+
+ println!("--- Sign installers");
+ codesign(&logger, &installer_paths)?;
+
+ Ok(())
+}
+
+fn build_installer(
+ variant_folder: &Path,
+ build_folder: &Path,
+ version: &str,
+ uninstaller: bool,
+) -> Result<()> {
+ let rendered_nsi = include_str!("../anki.template.nsi")
+ .replace("@@SRC@@", variant_folder.to_str().unwrap())
+ .replace("@@VERSION@@", version);
+ let rendered_nsi_path = build_folder.join("anki.nsi");
+ fs::write(&rendered_nsi_path, rendered_nsi).context("anki.nsi")?;
+ fs::write(
+ build_folder.join("fileassoc.nsh"),
+ include_str!("../fileassoc.nsh"),
+ )?;
+ let mut cmd = Command::new("c:/program files (x86)/nsis/makensis.exe");
+ cmd.arg("-V3");
+ if uninstaller {
+ cmd.arg("-DWRITE_UNINSTALLER");
+ };
+ if option_env!("NO_COMPRESS").is_some() {
+ cmd.arg("-DNO_COMPRESS");
+ }
+ cmd.arg(rendered_nsi_path);
+ let status = cmd.status()?;
+ if !status.success() {
+ bail!("makensis failed");
+ }
+ Ok(())
+}
+
+/// Copy everything at the provided path into the bundle dir.
+/// Excludes standard Bazel repo files.
+fn extend_app_contents(source: &Path, bundle_dir: &Path) -> Result<()> {
+ let status = Command::new("rsync")
+ .arg("-a")
+ .args(["--exclude", "BUILD.bazel", "--exclude", "WORKSPACE"])
+ .arg(format!("{}/", path_for_rsync(source, true)?))
+ .arg(format!("{}/", path_for_rsync(bundle_dir, true)?))
+ .status()?;
+ if !status.success() {
+ bail!("error syncing {source:?}");
+ }
+ Ok(())
+}
+
+/// Munge path into a format rsync expects on Windows.
+fn path_for_rsync(path: &Path, trailing_slash: bool) -> Result {
+ let mut components = path.components();
+ let mut drive = None;
+ if let Some(Component::Prefix(prefix)) = components.next() {
+ if let Prefix::Disk(letter) = prefix.kind() {
+ drive = Some(char::from(letter));
+ }
+ };
+ let drive = drive.context("missing drive letter")?;
+ let remaining_path: PathBuf = components.collect();
+ Ok(format!(
+ "/{}{}{}",
+ drive,
+ remaining_path
+ .to_str()
+ .context("remaining_path")?
+ .replace("\\", "/"),
+ if trailing_slash { "/" } else { "" }
+ ))
+}
+
+fn copy_in_audio(bundle_dir: &Path, bazel_external: &Path) -> Result<()> {
+ extend_app_contents(&bazel_external.join("audio_win_amd64"), bundle_dir)
+}
+
+fn codesign(logger: &Logger, paths: &[impl AsRef]) -> Result<()> {
+ if option_env!("ANKI_CODESIGN").is_none() {
+ return Ok(());
+ }
+ let cert = CodeSigningCertificate::Sha1Thumbprint(
+ SystemStore::My,
+ "60abdb9cb52b7dc13550e8838486a00e693770d9".into(),
+ );
+ let mut sign = SigntoolSign::new(cert);
+ sign.file_digest_algorithm("sha256")
+ .timestamp_server(TimestampServer::Rfc3161(
+ "http://time.certum.pl".into(),
+ "sha256".into(),
+ ))
+ .verbose();
+ paths.iter().for_each(|path| {
+ sign.sign_file(path);
+ });
+ sign.run(logger)
+}
+
+// FIXME: check uninstall.exe required or not
+fn build_manifest(base_path: &Path) -> Result<()> {
+ let mut buf = vec![];
+ for entry in WalkDir::new(base_path)
+ .min_depth(1)
+ .sort_by_file_name()
+ .into_iter()
+ {
+ let entry = entry?;
+ let path = entry.path();
+ let relative_path = path.strip_prefix(base_path)?;
+ write!(
+ &mut buf,
+ "{}\r\n",
+ relative_path.to_str().context("relative_path utf8")?
+ )?;
+ }
+ fs::write(base_path.join("anki.install-manifest"), buf)?;
+ Ok(())
+}
+
+#[cfg(test)]
+mod test {
+ #[allow(unused_imports)]
+ use super::*;
+
+ #[test]
+ #[cfg(windows)]
+ fn test_path_for_rsync() -> Result<()> {
+ assert_eq!(
+ path_for_rsync(Path::new("c:\\foo\\bar"), false)?,
+ "/C/foo/bar"
+ );
+ assert_eq!(
+ path_for_rsync(Path::new("c:\\foo\\bar"), true)?,
+ "/C/foo/bar/"
+ );
+
+ Ok(())
+ }
+}
diff --git a/qt/package/build.bat b/qt/package/build.bat
deleted file mode 100755
index 78c3ca4c2..000000000
--- a/qt/package/build.bat
+++ /dev/null
@@ -1,21 +0,0 @@
-:: ensure wheels are built and set up Rust env
-pushd ..\..
-call scripts\build || exit /b
-call scripts\cargo-env
-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 ..\..\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
deleted file mode 100644
index aafd6f246..000000000
--- a/qt/package/build.py
+++ /dev/null
@@ -1,264 +0,0 @@
-# Copyright: Ankitects Pty Ltd and contributors
-# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-
-
-from __future__ import annotations
-
-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"] += fr";{os.getenv('USERPROFILE')}\.cargo\bin"
- cargo_features = "build-mode-prebuilt-artifacts"
-else:
- python_bin_folder = venv / "bin"
- os.environ["PATH"] += f":{os.getenv('HOME')}/.cargo/bin"
- 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"
-pyqt6_folder_path = bazel_external / "pyqt6" / "PyQt6"
-is_lin = False
-arm64_linux = False
-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:
- is_lin = True
- if platform.machine() == "x86_64":
- os.environ["TARGET"] = "x86_64-unknown-linux-gnu"
- else:
- os.environ["TARGET"] = "aarch64-unknown-linux-gnu"
- pyqt5_folder_name = None
- pyqt6_folder_path = None
- arm64_linux = True
-
-
-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",
- "--git",
- "https://github.com/ankitects/PyOxidizer.git",
- "--rev",
- # when updating, make sure Cargo.toml updated too
- "eb26dd7cd1290de6503869f3d719eabcec45e139",
- "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
- )
- # pypi protobuf lacks C extension on darwin-arm; use a locally built version
- protobuf = Path.home() / "protobuf-3.19.1-cp39-cp39-macosx_11_0_arm64.whl"
- if protobuf.exists():
- subprocess.run(
- [pip, "install", "--force-reinstall", "--no-deps", protobuf], check=True
- )
- if arm64_linux:
- # orjson doesn't get packaged correctly; remove it and we'll
- # copy a copy in later
- subprocess.run([pip, "uninstall", "-y", "orjson"], 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 | None):
- if not output_folder.exists():
- output_folder.mkdir(parents=True)
- # PyQt
- if pyqt_src_path:
- 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")) + "/")
- elif is_lin:
- resources.append("lin/")
-
- subprocess.run(
- [
- "rsync",
- "-a",
- "--delete",
- "--exclude",
- "PyQt6",
- "--exclude",
- "PyQt5",
- *resources,
- adj_path_for_windows_rsync(output_folder) + "/",
- ],
- check=True,
- )
- # Linux ARM workarounds
- if arm64_linux:
- # copy orjson ends up broken; copy from venv
- subprocess.run(
- [
- "rsync",
- "-a",
- "--delete",
- os.path.expanduser("~/orjson"),
- output_folder / "lib/",
- ],
- check=True,
- )
- # Ensure all files are world-readable
- if not is_win:
- subprocess.run(["chmod", "-R", "a+r", output_folder])
-
-
-build_pyoxidizer()
-install_wheels_into_venv()
-build_artifacts()
-build_pkg()
-merge_into_dist(dist_folder / "std", pyqt6_folder_path)
-if pyqt5_folder_name:
- merge_into_dist(dist_folder / "alt", bazel_external / pyqt5_folder_name / "PyQt5")
diff --git a/qt/package/build.sh b/qt/package/build.sh
deleted file mode 100755
index bd0d64ed6..000000000
--- a/qt/package/build.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/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/buildmanifest.py b/qt/package/buildmanifest.py
deleted file mode 100644
index 3057879c9..000000000
--- a/qt/package/buildmanifest.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright: Ankitects Pty Ltd and contributors
-# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-
-import os
-import sys
-from pathlib import Path
-
-
-def build_manifest(top: Path) -> None:
- manifest = []
- for root, dirnames, fnames in os.walk(top, topdown=True):
- relroot = root[len(str(top)) + 1 :]
- # if not top level, add folder
- if relroot:
- manifest.append(relroot)
- # then the files
- for fname in fnames:
- path = os.path.join(relroot, fname)
- manifest.append(path)
-
- with open(top / "anki.install-manifest", "w") as file:
- file.write("\n".join(manifest) + "\n")
-
-
-if __name__ == "__main__":
- build_manifest(Path(sys.argv[1]))
diff --git a/qt/tests/run_format.py b/qt/tests/run_format.py
index a5039ad50..092e3d1b0 100644
--- a/qt/tests/run_format.py
+++ b/qt/tests/run_format.py
@@ -33,7 +33,7 @@ if __name__ == "__main__":
"aqt",
"tests",
"tools",
- "package",
+ "bundle",
]
+ args,
check=False,
@@ -51,7 +51,7 @@ if __name__ == "__main__":
"aqt",
"tests",
"tools",
- "package",
+ "bundle",
]
+ args,
check=False,
diff --git a/qt/tools/extract_sass_colors.py b/qt/tools/extract_sass_colors.py
index c13a1d7d9..219fd6a26 100644
--- a/qt/tools/extract_sass_colors.py
+++ b/qt/tools/extract_sass_colors.py
@@ -22,6 +22,7 @@ for line in open(input_scss):
and not ":root" in line
and "Copyright" not in line
and "License" not in line
+ and "color-scheme" not in line
):
print("failed to match", line)
continue
diff --git a/repos.bzl b/repos.bzl
index 8cb8619e9..85db1ae59 100644
--- a/repos.bzl
+++ b/repos.bzl
@@ -151,3 +151,98 @@ exports_files(["l10n.toml"])
],
sha256 = qtftl_i18n_zip_csum,
)
+
+ # binary bundle
+ ################
+
+ maybe(
+ http_archive,
+ name = "pyoxidizer",
+ sha256 = "9f7951473d88c7989dc80199146f82020226a3b2425474fd33b6bcbd8fdd1b1c",
+ urls = [
+ # when changing this, the commit hash needs to be updated in qt/bundle/Cargo.toml
+ "https://github.com/ankitects/PyOxidizer/archive/refs/tags/anki-2021-12-08.tar.gz",
+ ],
+ strip_prefix = "PyOxidizer-anki-2021-12-08",
+ build_file_content = " ",
+ )
+
+ maybe(
+ http_archive,
+ name = "bundle_extras_linux_amd64",
+ build_file_content = " ",
+ urls = [
+ "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2022-02-09/linux-amd64.tar.gz",
+ ],
+ sha256 = "cbfb41fb750ae19b381f8137bd307e1167fdc68420052977f6e1887537a131b0",
+ )
+
+ maybe(
+ http_archive,
+ name = "audio_win_amd64",
+ build_file_content = " ",
+ urls = [
+ "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2022-02-09/audio-win-amd64.tar.gz",
+ ],
+ sha256 = "0815a601baba05e03bc36b568cdc2332b1cf4aa17125fc33c69de125f8dd687f",
+ )
+
+ maybe(
+ http_archive,
+ name = "protobuf_wheel_mac_arm64",
+ build_file_content = " ",
+ urls = [
+ "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2022-02-09/protobuf-wheel-mac-arm64.tar",
+ ],
+ sha256 = "401d1cd6d949af463b3945f0d5dc887185b27fa5478cb6847bf94f680ea797b4",
+ )
+
+ maybe(
+ http_archive,
+ name = "audio_mac_amd64",
+ build_file_content = " ",
+ urls = [
+ "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2022-02-09/audio-mac-amd64.tar.gz",
+ ],
+ sha256 = "d9310cbd6bed09d6d36deb8b7611bffbd161628512b1bf8d7becfdf78b5cd1dd",
+ )
+
+ maybe(
+ http_archive,
+ name = "audio_mac_arm64",
+ build_file_content = " ",
+ urls = [
+ "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2022-02-09/audio-mac-arm64.tar.gz",
+ ],
+ sha256 = "c30a772132a16fa79d9a1e60f5dce2f91fe8077e2709a8f39ef499d49f6a4b0e",
+ )
+
+ maybe(
+ http_archive,
+ name = "pyqt6.2_mac_bundle_amd64",
+ build_file_content = " ",
+ urls = [
+ "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2022-02-09/pyqt6.2-mac-amd64.tar.gz",
+ ],
+ sha256 = "c7bf899eee33fcb3b5848f5d3e5fc390012efc05c2308e4349b7bbd5939c85f0",
+ )
+
+ maybe(
+ http_archive,
+ name = "pyqt6.2_mac_bundle_arm64",
+ build_file_content = " ",
+ urls = [
+ "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2022-02-09/pyqt6.2-mac-arm64.tar.gz",
+ ],
+ sha256 = "7a4b7d5bd65c83fd16cf7e56929183ef0d1d7bb67f9deea8f2482d7378e0ea02",
+ )
+
+ maybe(
+ http_archive,
+ name = "pyqt5.14_mac_bundle_amd64",
+ build_file_content = " ",
+ urls = [
+ "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2022-02-09/pyqt5.14-mac-amd64.tar.gz",
+ ],
+ sha256 = "474951bed79ddb9570ee4c5a6079041772551ea77e77171d9e33d6f5e7877ec1",
+ )
diff --git a/run.bat b/run.bat
index d5d9a78b0..910998fb3 100755
--- a/run.bat
+++ b/run.bat
@@ -1,2 +1,7 @@
+@echo off
+pushd "%~dp0"
+call tools\setup-env.bat
+
set PYTHONWARNINGS=default
-call .\bazel.bat run %BUILDARGS% //qt:runanki -k -- %*
+bazel run %BUILDARGS% //qt:runanki -k -- %* || exit /b 1
+popd
diff --git a/scripts/build b/scripts/build
deleted file mode 100755
index cb6081935..000000000
--- a/scripts/build
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/bash
-
-set -e
-
-test -e WORKSPACE || (
- echo "Run from project root"
- exit 1
-)
-
-rm -rf bazel-dist
-bazel build --config opt dist
-tar xvf .bazel/bin/dist.tar
diff --git a/scripts/build.bat b/scripts/build.bat
deleted file mode 100755
index ca43defa9..000000000
--- a/scripts/build.bat
+++ /dev/null
@@ -1,12 +0,0 @@
-@echo off
-
-if not exist WORKSPACE (
- echo Run from project root
- exit /b 1
-)
-
-rd /s /q bazel-dist
-
-set BUILDARGS=-k -c opt dist --color=yes
-call .\bazel build %BUILDARGS% || exit /b 1
-tar xvf ..\bazel\anki\bin\dist.tar || exit /b 1
diff --git a/scripts/cargo-env.bat b/scripts/cargo-env.bat
deleted file mode 100644
index 0ca775e93..000000000
--- a/scripts/cargo-env.bat
+++ /dev/null
@@ -1,4 +0,0 @@
-rem Run this from the repo root folder
-
-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/docker/README.md b/scripts/docker/README.md
deleted file mode 100644
index 83d02658b..000000000
--- a/scripts/docker/README.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Building in Docker
-
-This folder contains a script for building Anki inside a Docker container.
-It works by creating an image with the required dependencies, and then runs the
-build with the source folder mounted into the image. This will cause files to be
-written into `bazel-\*` and `node_modules` in the source folder as the build proceeds.
-The advantage of doing it this way is that most of the efficiency of building
-outside Docker is retained - you can make minor changes and run the build again,
-and only the changed parts need to be rebuilt.
-
-If you're looking for a fully isolated build, [this other
-approach](../../docs/docker/README.md) in the docs folder may suit you better. As
-it also includes runtime dependencies, it may be a useful reference for libraries
-you'll need to install before running Anki.
-
-# Usage
-
-Ensure Docker is installed on your machine, and your user has permission to connect
-to Docker. Then run the following command from the root folder of this source repo:
-
-```
-$ scripts/docker/build.sh amd64
-```
-
-The resulting wheels will be written into bazel-dist. See
-[Development](../docs/development.md) for information on how to install them.
-
-If you're on an ARM Linux machine, replace amd64 with arm64.
diff --git a/scripts/docker/build-entrypoint b/scripts/docker/build-entrypoint
deleted file mode 100644
index 6a97fbc20..000000000
--- a/scripts/docker/build-entrypoint
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/bash
-
-set -e
-
-rm -rf bazel-dist
-bazel build -c opt dist --symlink_prefix=bazel-docker/links/ \
- --experimental_no_product_name_out_symlink
-tar xvf bazel-docker/links/bin/dist.tar
-if [ "$PACKAGE" != "" ]; then
- (cd qt/package && ./build.sh)
-fi
-bazel shutdown
diff --git a/scripts/docker/build.sh b/scripts/docker/build.sh
deleted file mode 100755
index 2c3968b55..000000000
--- a/scripts/docker/build.sh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/bash
-
-set -e
-
-test -e WORKSPACE || (
- echo "Run from project root"
- exit 1
-)
-
-arch=$1
-
-if [ "$arch" != "amd64" -a "$arch" != "arm64" ]; then
- echo "usage: build [amd64|arm64]"
- exit 1
-fi
-
-rm -rf bazel-dist
-
-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 -e PACKAGE=$PACKAGE \
- --mount type=bind,source="$(pwd)",target=/code \
- ankibuild
diff --git a/scripts/python.bat b/scripts/python.bat
deleted file mode 100755
index 44d6a5d3c..000000000
--- a/scripts/python.bat
+++ /dev/null
@@ -1 +0,0 @@
-call bazel run python --ui_event_filters=-INFO -- %*
diff --git a/scripts/run-qt5.14 b/scripts/run-qt5.14
deleted file mode 100755
index 6eaba20e9..000000000
--- a/scripts/run-qt5.14
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-
-# note: 5.14 is not available on Windows for Python 3.9
-
-set -e
-
-export PYTHONWARNINGS=default
-bazel run $BUILDARGS //qt:runanki_qt514 -- $*
diff --git a/scripts/run-qt5.15 b/scripts/run-qt5.15
deleted file mode 100755
index e04622eeb..000000000
--- a/scripts/run-qt5.15
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/bash
-
-set -e
-
-export PYTHONWARNINGS=default
-bazel run $BUILDARGS //qt:runanki_qt515 -- $*
diff --git a/scripts/run-qt5.15.bat b/scripts/run-qt5.15.bat
deleted file mode 100755
index 252685f03..000000000
--- a/scripts/run-qt5.15.bat
+++ /dev/null
@@ -1,4 +0,0 @@
-REM run this from the scripts folder, not from root
-
-set PYTHONWARNINGS=default
-call ..\bazel.bat run %BUILDARGS% //qt:runanki_qt515 -k -- %*
diff --git a/scripts/runopt.bat b/scripts/runopt.bat
deleted file mode 100755
index 9027d87d7..000000000
--- a/scripts/runopt.bat
+++ /dev/null
@@ -1,2 +0,0 @@
-set BUILDARGS=-c opt
-call .\run.bat %*
diff --git a/scripts/BUILD.bazel b/tools/BUILD.bazel
similarity index 100%
rename from scripts/BUILD.bazel
rename to tools/BUILD.bazel
diff --git a/tools/bazel b/tools/bazel
new file mode 100755
index 000000000..4e99266e6
--- /dev/null
+++ b/tools/bazel
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+set -e
+
+# When building under Rosetta, use a separate output root, so that repo rules don't
+# need to be run again when switching between x86_64 and arm64 builds.
+extra_args=""
+if [[ $OSTYPE == 'darwin'* ]]; then
+ if [ $(uname -m) = x86_64 -a "$(sysctl -in sysctl.proc_translated)" = 1 ]; then
+ extra_args="--output_base=$HOME/.cache/anki-rosetta"
+ fi
+fi
+
+# Bazelisk will place the tools folder at the front of the path. This breaks
+# genrule() invocations like //:buildinfo, as they call 'bazel run python', which
+# fails as BAZEL_REAL is not passed to the child process. Work around it by removing
+# the tools folder from the path.
+export PATH=$(echo "$PATH" | sed 's@^.*/tools:@@')
+
+exec $BAZEL_REAL $extra_args "$@"
diff --git a/tools/build b/tools/build
new file mode 100755
index 000000000..250c4f4c3
--- /dev/null
+++ b/tools/build
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+set -e
+
+test -e WORKSPACE || (
+ echo "Run from project root"
+ exit 1
+)
+
+cwd=$(pwd)
+build=.bazel/out/dist
+
+# we remove wheels between runs, so old builds
+# don't break 'pip install dist/*.whl'
+rm -rf ${build}/*.whl
+
+bazel build --config opt wheels
+mkdir -p $build
+cd $build && tar xf ${cwd}/.bazel/bin/wheels.tar
+
+echo "wheels are in .bazel/out/dist"
diff --git a/tools/build.bat b/tools/build.bat
new file mode 100755
index 000000000..d9f5c0c2c
--- /dev/null
+++ b/tools/build.bat
@@ -0,0 +1,18 @@
+@echo off
+pushd "%~dp0"\..
+call tools\setup-env.bat
+
+echo --- Build wheels
+
+set cwd=%CD%
+set dist=.bazel\out\dist
+
+bazel build -c opt wheels --color=yes || exit /b 1
+if exist %dist% (
+ rd /s /q %dist% || exit /b 1
+)
+md %dist% || exit /b 1
+cd %dist%
+tar xvf %cwd%\.bazel\bin\wheels.tar || exit /b 1
+echo wheels are in %dist%
+popd
diff --git a/scripts/buildinfo.py b/tools/buildinfo.py
similarity index 100%
rename from scripts/buildinfo.py
rename to tools/buildinfo.py
diff --git a/tools/bundle b/tools/bundle
new file mode 100755
index 000000000..7843a6654
--- /dev/null
+++ b/tools/bundle
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+set -e
+
+cd $(dirname $0)
+ROOT=$(pwd)/..
+OUTPUT_ROOT=$ROOT/.bazel/out/build
+VENV=$OUTPUT_ROOT/venv-$(uname -m)
+BAZEL_EXTERNAL=$(bazel info output_base --ui_event_filters=-INFO)/external
+
+echo "--- Building wheels for $(uname -m)"
+(cd $ROOT && ./tools/build)
+
+echo "--- Setup venv"
+test -d $VENV || (
+ mkdir -p $OUTPUT_ROOT
+ (cd $ROOT && ./tools/python -m venv $VENV)
+)
+
+echo "--- Fetch extra deps"
+bazel query @pyoxidizer//:*
+if [[ "$OSTYPE" == "darwin"* ]]; then
+ if [ $(uname -m) != "arm64" ]; then
+ bazel query @audio_mac_amd64//:* > /dev/null
+ bazel query @pyqt6.2_mac_bundle_amd64//:* > /dev/null
+ bazel query @pyqt5.14_mac_bundle_amd64//:* > /dev/null
+ else
+ bazel query @audio_mac_arm64//:* > /dev/null
+ bazel query @pyqt6.2_mac_bundle_arm64//:* > /dev/null
+ bazel query @protobuf_wheel_mac_arm64//:* > /dev/null
+
+ fi
+else
+ bazel query @pyqt515//:* > /dev/null
+ bazel query @bundle_extras_linux_amd64//:* > /dev/null
+fi
+
+. $ROOT/tools/cargo-env
+(cd $ROOT/qt/bundle && $VENV/bin/python build.py $ROOT $BAZEL_EXTERNAL)
diff --git a/tools/bundle.bat b/tools/bundle.bat
new file mode 100755
index 000000000..c1dbe0765
--- /dev/null
+++ b/tools/bundle.bat
@@ -0,0 +1,33 @@
+@echo off
+pushd "%~dp0"\..
+call tools\setup-env.bat
+
+:: ensure wheels are built and set up Rust env
+call tools\build || exit /b 1
+call tools\cargo-env
+set ROOT=%CD%
+
+:: ensure venv exists
+echo --- Setup venv
+set OUTPUT_ROOT=%ROOT%\.bazel\out\build
+set VENV=%OUTPUT_ROOT%\venv-AMD64
+if not exist %VENV% (
+ mkdir %OUTPUT_ROOT%
+ call tools\python -m venv %VENV% || exit /b
+)
+
+:: pyoxidizer requires python.org for build
+set PATH=\python39;%PATH%
+
+:: run the rest of the build in Python
+echo --- Fetching extra deps
+FOR /F "tokens=*" %%g IN ('bazel info output_base --ui_event_filters=-INFO') do (SET BAZEL_EXTERNAL=%%g/external)
+bazel query @pyqt515//:* > nul
+bazel query @pyoxidizer//:* > nul
+bazel query @audio_win_amd64//:* > nul
+
+echo --- Build bundle
+pushd qt\bundle
+%VENV%\scripts\python build.py %ROOT% %BAZEL_EXTERNAL% || exit /b
+popd
+popd
\ No newline at end of file
diff --git a/scripts/cargo-env b/tools/cargo-env
similarity index 85%
rename from scripts/cargo-env
rename to tools/cargo-env
index b987d169a..e242bbd1e 100755
--- a/scripts/cargo-env
+++ b/tools/cargo-env
@@ -4,19 +4,19 @@
# 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
+# $ . tools/cargo-env
# $ (cd rslib && cargo check)
BAZEL_EXTERNAL=$(bazel info output_base --ui_event_filters=-INFO)/external
if [[ "$OSTYPE" == "darwin"* ]]; then
- if [ "$(arch)" == "i386" ]; then
+ if [ "$(uname -m)" == "x86_64" ]; then
export PATH="$BAZEL_EXTERNAL/rust_darwin_x86_64/bin:$PATH"
else
export PATH="$BAZEL_EXTERNAL/rust_darwin_aarch64/bin:$PATH"
fi
else
- if [ "$(arch)" == "aarch64" ]; then
+ if [ "$(uname -m)" == "aarch64" ]; then
export PATH="$BAZEL_EXTERNAL/rust_linux_aarch64/bin:$PATH"
else
export PATH="$BAZEL_EXTERNAL/rust_linux_x86_64/bin:$PATH"
diff --git a/tools/cargo-env.bat b/tools/cargo-env.bat
new file mode 100644
index 000000000..474793e1e
--- /dev/null
+++ b/tools/cargo-env.bat
@@ -0,0 +1,7 @@
+@echo off
+pushd "%~dp0"\..
+call tools\setup-env.bat
+
+FOR /F "tokens=*" %%g IN ('bazel info output_base --ui_event_filters=-INFO') do (SET BAZEL_EXTERNAL=%%g/external)
+set PATH=%BAZEL_EXTERNAL%\rust_windows_x86_64\bin;%PATH%
+popd
diff --git a/scripts/copyright_headers.py b/tools/copyright_headers.py
similarity index 96%
rename from scripts/copyright_headers.py
rename to tools/copyright_headers.py
index 5cb1dfc06..44aea6aae 100644
--- a/scripts/copyright_headers.py
+++ b/tools/copyright_headers.py
@@ -14,8 +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",
+ "qt/bundle/build.rs",
+ "qt/bundle/src/main.rs",
}
ignored_folders = [
diff --git a/scripts/mypy b/tools/mypy
similarity index 100%
rename from scripts/mypy
rename to tools/mypy
diff --git a/scripts/mypy-watch b/tools/mypy-watch
similarity index 100%
rename from scripts/mypy-watch
rename to tools/mypy-watch
diff --git a/scripts/node-env b/tools/node-env
similarity index 96%
rename from scripts/node-env
rename to tools/node-env
index 9f72b4472..37d8b00cb 100755
--- a/scripts/node-env
+++ b/tools/node-env
@@ -4,7 +4,7 @@
# quick access on a machine that does not have Node installed separately.
# Eg:
-# $ . scripts/node-env
+# $ . tools/node-env
# $ (cd ts && yarn)
BAZEL_EXTERNAL=$(bazel info output_base --ui_event_filters=-INFO)/external
diff --git a/scripts/python b/tools/python
similarity index 100%
rename from scripts/python
rename to tools/python
diff --git a/tools/python.bat b/tools/python.bat
new file mode 100755
index 000000000..b4c237f09
--- /dev/null
+++ b/tools/python.bat
@@ -0,0 +1,6 @@
+@echo off
+pushd "%~dp0"\..
+call tools\setup-env.bat
+
+bazel run python --ui_event_filters=-INFO -- %*
+popd
diff --git a/tools/run-qt5.14 b/tools/run-qt5.14
new file mode 100755
index 000000000..d039be07a
--- /dev/null
+++ b/tools/run-qt5.14
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# note: 5.14 is not available on Windows for Python 3.9
+
+set -e
+
+# automatically switch to an x86_64 build on macOS, as this
+# Qt version does not work on arm64
+prefix=""
+if [[ $OSTYPE == 'darwin'* ]]; then
+ if [ $(uname -m) = arm64 ]; then
+ prefix="arch -arch x86_64"
+ fi
+fi
+
+export PYTHONWARNINGS=default
+$prefix bazel run $BUILDARGS //qt:runanki_qt514 -- $*
diff --git a/tools/run-qt5.15 b/tools/run-qt5.15
new file mode 100755
index 000000000..c22c47b68
--- /dev/null
+++ b/tools/run-qt5.15
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+set -e
+
+# automatically switch to an x86_64 build on macOS, as this
+# Qt version does not work on arm64
+prefix=""
+if [[ $OSTYPE == 'darwin'* ]]; then
+ if [ $(uname -m) = arm64 ]; then
+ prefix="arch -arch x86_64"
+ fi
+fi
+
+export PYTHONWARNINGS=default
+$prefix bazel run $BUILDARGS //qt:runanki_qt515 -- $*
diff --git a/tools/run-qt5.15.bat b/tools/run-qt5.15.bat
new file mode 100755
index 000000000..641e6ab93
--- /dev/null
+++ b/tools/run-qt5.15.bat
@@ -0,0 +1,7 @@
+@echo off
+pushd "%~dp0"\..
+call tools\setup-env.bat
+
+set PYTHONWARNINGS=default
+bazel run %BUILDARGS% //qt:runanki_qt515 -k -- %*
+popd
\ No newline at end of file
diff --git a/scripts/runopt b/tools/runopt
similarity index 100%
rename from scripts/runopt
rename to tools/runopt
diff --git a/tools/runopt.bat b/tools/runopt.bat
new file mode 100755
index 000000000..47b503ac2
--- /dev/null
+++ b/tools/runopt.bat
@@ -0,0 +1,7 @@
+@echo off
+pushd "%~dp0"\..
+call tools\setup-env.bat
+
+set BUILDARGS=-c opt
+call .\run.bat %*
+popd
\ No newline at end of file
diff --git a/tools/setup-env.bat b/tools/setup-env.bat
new file mode 100644
index 000000000..6e6435eab
--- /dev/null
+++ b/tools/setup-env.bat
@@ -0,0 +1,16 @@
+@echo off
+pushd "%~dp0"\..
+
+REM add msys/bazel to path if they're not already on it
+where /q bazel || (
+ set PATH=c:\msys64\usr\bin;c:\bazel;%PATH%
+)
+
+if not exist windows.bazelrc (
+ rem By default, Bazel will place build files in c:\users\\_bazel_, and this
+ rem can lead to build failures when the path names grow too long. So on Windows, the
+ rem default storage location is \bazel\anki instead.
+ echo startup --output_user_root=\\bazel\\anki > windows.bazelrc
+)
+
+popd
diff --git a/scripts/status.sh b/tools/status.sh
similarity index 100%
rename from scripts/status.sh
rename to tools/status.sh
diff --git a/scripts/svelte-check b/tools/svelte-check
similarity index 85%
rename from scripts/svelte-check
rename to tools/svelte-check
index 0f9732cd7..287230191 100755
--- a/scripts/svelte-check
+++ b/tools/svelte-check
@@ -1,6 +1,6 @@
#!/bin/bash
-. scripts/node-env
+. tools/node-env
SASS_PATH=ts/sass:$(pwd)/.bazel/bin/ts/sass \
node_modules/.bin/svelte-check \
--workspace ts
diff --git a/scripts/ts-run b/tools/ts-run
similarity index 100%
rename from scripts/ts-run
rename to tools/ts-run
diff --git a/scripts/ts-watch b/tools/ts-watch
similarity index 100%
rename from scripts/ts-watch
rename to tools/ts-watch