1{
2 lib,
3 stdenv,
4
5 writableTmpDirAsHomeHook,
6 buildEnv,
7 cargo,
8 fetchFromGitHub,
9 installShellFiles,
10 lame,
11 mpv-unwrapped,
12 ninja,
13 callPackage,
14 nixosTests,
15 nodejs,
16 jq,
17 protobuf,
18 python3,
19 qt6,
20 rsync,
21 rustPlatform,
22 writeShellScriptBin,
23 yarn,
24 yarn-berry_4,
25
26 swift,
27
28 mesa,
29}:
30
31let
32 yarn-berry = yarn-berry_4;
33
34 pname = "anki";
35 version = "25.02.5";
36 rev = "29192d156ae60d6ce35e80ccf815a8331c9db724";
37
38 srcHash = "sha256-lx3tK57gcQpwmiqUzO6iU7sE31LPFp6s80prYaB2jHE=";
39 cargoHash = "sha256-BPCfeUiZ23FdZaF+zDUrRZchauNZWQ3gSO+Uo9WRPes=";
40 yarnHash = "sha256-3G+9N3xOzog3XDCKDQJCY/6CB3i6oXixRgxEyv7OG3U=";
41
42 src = fetchFromGitHub {
43 owner = "ankitects";
44 repo = "anki";
45 rev = version;
46 hash = srcHash;
47 fetchSubmodules = true;
48 };
49
50 cargoDeps = rustPlatform.fetchCargoVendor {
51 inherit pname version src;
52 hash = cargoHash;
53 };
54
55 # a wrapper for yarn to skip 'install'
56 # We do this because we need to patchShebangs after install, so we do it
57 # ourselves beforehand.
58 # We also, confusingly, have to use yarn-berry to handle the lockfile (anki's
59 # lockfile is too new for yarn), but have to use 'yarn' here, because anki's
60 # build system uses yarn-1 style flags and such.
61 # I think what's going on here is that yarn-1 in anki's normal build system
62 # ends up noticing the yarn-file is too new and shelling out to yarn-berry
63 # itself.
64 noInstallYarn = writeShellScriptBin "yarn" ''
65 [[ "$1" == "install" ]] && exit 0
66 exec ${yarn}/bin/yarn "$@"
67 '';
68
69 anki-build-python = python3.withPackages (ps: with ps; [ mypy-protobuf ]);
70
71 pyEnv = buildEnv {
72 name = "anki-pyenv-${version}";
73 paths = with python3.pkgs; [
74 pip
75 anki-build-python
76 ];
77 pathsToLink = [ "/bin" ];
78 };
79in
80python3.pkgs.buildPythonApplication rec {
81 format = "setuptools";
82 inherit pname version;
83
84 outputs = [
85 "out"
86 "doc"
87 "man"
88 ];
89
90 inherit src;
91
92 patches = [
93 ./patches/disable-auto-update.patch
94 ./patches/remove-the-gl-library-workaround.patch
95 ./patches/skip-formatting-python-code.patch
96 # Used in with-addons.nix
97 ./patches/allow-setting-addons-folder.patch
98 ];
99
100 inherit cargoDeps;
101
102 missingHashes = ./missing-hashes.json;
103 yarnOfflineCache = yarn-berry.fetchYarnBerryDeps {
104 inherit missingHashes;
105 yarnLock = "${src}/yarn.lock";
106 hash = yarnHash;
107 };
108
109 nativeBuildInputs = [
110 cargo
111 installShellFiles
112 jq
113 ninja
114 nodejs
115 qt6.wrapQtAppsHook
116 rsync
117 rustPlatform.cargoSetupHook
118 writableTmpDirAsHomeHook
119 yarn-berry_4.yarnBerryConfigHook
120 ]
121 ++ lib.optional stdenv.hostPlatform.isDarwin swift;
122
123 buildInputs = [
124 qt6.qtbase
125 qt6.qtsvg
126 ]
127 ++ lib.optional stdenv.hostPlatform.isLinux qt6.qtwayland;
128
129 propagatedBuildInputs = with python3.pkgs; [
130 # This rather long list came from running:
131 # grep --no-filename -oE "^[^ =]*" python/{requirements.base.txt,requirements.bundle.txt,requirements.qt6_lin.txt} | \
132 # sort | uniq | grep -v "^#$"
133 # in their repo at the git tag for this version
134 # There's probably a more elegant way, but the above extracted all the
135 # names, without version numbers, of their python dependencies. The hope is
136 # that nixpkgs versions are "close enough"
137 # I then removed the ones the check phase failed on (pythonCatchConflictsPhase)
138 attrs
139 beautifulsoup4
140 blinker
141 build
142 certifi
143 charset-normalizer
144 click
145 colorama
146 decorator
147 flask
148 flask-cors
149 google-api-python-client
150 idna
151 importlib-metadata
152 itsdangerous
153 jinja2
154 jsonschema
155 markdown
156 markupsafe
157 orjson
158 packaging
159 pip
160 pip-system-certs
161 pip-tools
162 protobuf
163 pyproject-hooks
164 pyqt6
165 pyqt6-sip
166 pyqt6-webengine
167 pyrsistent
168 pysocks
169 requests
170 send2trash
171 setuptools
172 soupsieve
173 tomli
174 urllib3
175 waitress
176 werkzeug
177 wheel
178 wrapt
179 zipp
180 ];
181
182 nativeCheckInputs = with python3.pkgs; [
183 pytest
184 mock
185 astroid
186 ];
187
188 # tests fail with too many open files
189 # TODO: verify if this is still true (I can't, no mac)
190 doCheck = !stdenv.hostPlatform.isDarwin;
191
192 checkFlags = [
193 # this test is flaky, see https://github.com/ankitects/anki/issues/3619
194 # also remove from anki-sync-server when removing this
195 "--skip=deckconfig::update::test::should_keep_at_least_one_remaining_relearning_step"
196 ];
197
198 dontUseNinjaInstall = false;
199 dontWrapQtApps = true;
200
201 env = {
202 # Activate optimizations
203 RELEASE = true;
204
205 # https://github.com/ankitects/anki/blob/24.11/docs/linux.md#packaging-considerations
206 OFFLINE_BUILD = "1";
207 NODE_BINARY = lib.getExe nodejs;
208 PROTOC_BINARY = lib.getExe protobuf;
209 PYTHON_BINARY = lib.getExe python3;
210 };
211
212 buildPhase = ''
213 export RUST_BACKTRACE=1
214 export RUST_LOG=debug
215
216 mkdir -p out/pylib/anki .git
217
218 echo ${builtins.substring 0 8 rev} > out/buildhash
219
220 ln -vsf ${pyEnv} ./out/pyenv
221
222 mv node_modules out
223
224 # Run everything else
225 patchShebangs ./ninja
226
227 # Necessary for yarn to not complain about 'corepack'
228 jq 'del(.packageManager)' package.json > package.json.tmp && mv package.json.tmp package.json
229 YARN_BINARY="${lib.getExe noInstallYarn}" PIP_USER=1 ./ninja build wheels
230 '';
231
232 # mimic https://github.com/ankitects/anki/blob/76d8807315fcc2675e7fa44d9ddf3d4608efc487/build/ninja_gen/src/python.rs#L232-L250
233 checkPhase =
234 let
235 disabledTestsString =
236 lib.pipe
237 [
238 # assumes / is not writeable, somehow fails on nix-portable brwap
239 "test_create_open"
240 ]
241 [
242 (lib.map (test: "not ${test}"))
243 (lib.concatStringsSep " and ")
244 lib.escapeShellArg
245 ];
246
247 in
248 ''
249 runHook preCheck
250 HOME=$TMP ANKI_TEST_MODE=1 PYTHONPATH=$PYTHONPATH:$PWD/out/pylib \
251 pytest -p no:cacheprovider pylib/tests -k ${disabledTestsString}
252 HOME=$TMP ANKI_TEST_MODE=1 PYTHONPATH=$PYTHONPATH:$PWD/out/pylib:$PWD/pylib:$PWD/out/qt \
253 pytest -p no:cacheprovider qt/tests -k ${disabledTestsString}
254 runHook postCheck
255 '';
256
257 preInstall = ''
258 mkdir dist
259 mv out/wheels/* dist
260 '';
261
262 postInstall = ''
263 install -D -t $out/share/applications qt/bundle/lin/anki.desktop
264 install -D -t $doc/share/doc/anki README* LICENSE*
265 install -D -t $out/share/mime/packages qt/bundle/lin/anki.xml
266 install -D -t $out/share/pixmaps qt/bundle/lin/anki.{png,xpm}
267 installManPage qt/bundle/lin/anki.1
268 '';
269
270 preFixup = ''
271 makeWrapperArgs+=(
272 "''${qtWrapperArgs[@]}"
273 --prefix PATH ':' "${lame}/bin:${mpv-unwrapped}/bin"
274 )
275 '';
276
277 passthru = {
278 withAddons = ankiAddons: callPackage ./with-addons.nix { inherit ankiAddons; };
279 tests.anki-sync-server = nixosTests.anki-sync-server;
280 };
281
282 meta = with lib; {
283 description = "Spaced repetition flashcard program";
284 mainProgram = "anki";
285 longDescription = ''
286 Anki is a program which makes remembering things easy. Because it is a lot
287 more efficient than traditional study methods, you can either greatly
288 decrease your time spent studying, or greatly increase the amount you learn.
289
290 Anyone who needs to remember things in their daily life can benefit from
291 Anki. Since it is content-agnostic and supports images, audio, videos and
292 scientific markup (via LaTeX), the possibilities are endless. For example:
293 learning a language, studying for medical and law exams, memorizing
294 people's names and faces, brushing up on geography, mastering long poems,
295 or even practicing guitar chords!
296 '';
297 homepage = "https://apps.ankiweb.net";
298 license = licenses.agpl3Plus;
299 inherit (mesa.meta) platforms;
300 maintainers = with maintainers; [
301 euank
302 junestepp
303 oxij
304 ];
305 # Reported to crash at launch on darwin (as of 2.1.65)
306 broken = stdenv.hostPlatform.isDarwin;
307 };
308}