anki: add withAddons

`anki.withAddons` creates a wrapped version of Anki with
all the addons provided to the function installed.
This commit is contained in:
June Stepp 2025-05-03 22:44:07 -05:00
parent ebed8fb6dc
commit a6826b4103
No known key found for this signature in database
GPG Key ID: 348C6EB339AEC582
6 changed files with 261 additions and 0 deletions

View File

@ -0,0 +1,126 @@
{
lib,
stdenv,
symlinkJoin,
lndir,
formats,
runCommand,
}:
{
buildAnkiAddon = lib.extendMkDerivation {
constructDrv = stdenv.mkDerivation;
extendDrvArgs =
finalAttrs:
{
pname,
version,
src,
sourceRoot ? "",
configurePhase ? ''
runHook preConfigure
runHook postConfigure
'',
buildPhase ? ''
runHook preBuild
runHook postBuild
'',
dontPatchELF ? true,
dontStrip ? true,
nativeBuildInputs ? [ ],
passthru ? { },
meta ? { },
# Script run after "user_files" folder is populated.
# Used when an add-on needs to process and change "user_files" based
# on what the user added to it.
processUserFiles ? "",
...
}:
{
inherit
version
src
sourceRoot
configurePhase
buildPhase
dontPatchELF
dontStrip
nativeBuildInputs
;
pname = "anki-addon-${pname}";
installPrefix = "share/anki/addons/${pname}";
installPhase = ''
runHook preInstall
mkdir -p "$out/$installPrefix"
find . -mindepth 1 -maxdepth 1 | xargs -d'\n' mv -t "$out/$installPrefix/"
runHook postInstall
'';
passthru = {
withConfig =
{
# JSON add-on config. The available options for an add-on are in its
# config.json file.
# See https://addon-docs.ankiweb.net/addon-config.html#config-json
config ? { },
# Path to a folder to be merged with the add-on "user_files" folder.
# See https://addon-docs.ankiweb.net/addon-config.html#user-files.
userFiles ? null,
}:
let
metaConfigFormat = formats.json { };
addonMetaConfig = metaConfigFormat.generate "meta.json" { inherit config; };
in
symlinkJoin {
pname = "${finalAttrs.pname}-with-config";
inherit (finalAttrs) version meta;
paths = [
finalAttrs.finalPackage
];
postBuild = ''
cd $out/${finalAttrs.installPrefix}
rm -f meta.json
ln -s ${addonMetaConfig} meta.json
mkdir -p user_files
${
if (userFiles != null) then
''
${lndir}/bin/lndir -silent "${userFiles}" user_files
''
else
""
}
${processUserFiles}
'';
};
} // passthru;
meta = {
platforms = lib.platforms.all;
} // meta;
};
};
buildAnkiAddonsDir =
addonPackages:
let
addonDirs = map (pkg: "${pkg}/share/anki/addons") addonPackages;
addons = lib.concatMapStringsSep " " (p: "${p}/*") addonDirs;
in
runCommand "anki-addons" { } ''
mkdir $out
[[ '${addons}' ]] || exit 0
for addon in ${addons}; do
ln -s "$addon" $out/
done
'';
}

View File

@ -0,0 +1,6 @@
{
callPackage,
}:
{
}

View File

@ -10,6 +10,7 @@
lame,
mpv-unwrapped,
ninja,
callPackage,
nixosTests,
nodejs,
nodejs-slim,
@ -129,6 +130,8 @@ python3.pkgs.buildPythonApplication {
./patches/disable-auto-update.patch
./patches/remove-the-gl-library-workaround.patch
./patches/skip-formatting-python-code.patch
# Used in with-addons.nix
./patches/allow-setting-addons-folder.patch
];
inherit cargoDeps yarnOfflineCache;
@ -301,6 +304,7 @@ python3.pkgs.buildPythonApplication {
'';
passthru = {
withAddons = ankiAddons: callPackage ./with-addons.nix { inherit ankiAddons; };
tests.anki-sync-server = nixosTests.anki-sync-server;
};
@ -324,6 +328,7 @@ python3.pkgs.buildPythonApplication {
inherit (mesa.meta) platforms;
maintainers = with maintainers; [
euank
junestepp
oxij
];
# Reported to crash at launch on darwin (as of 2.1.65)

View File

@ -0,0 +1,15 @@
diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py
index 469908c1b2..34612d6e08 100644
--- a/qt/aqt/profiles.py
+++ b/qt/aqt/profiles.py
@@ -310,7 +310,9 @@ def profileFolder(self, create: bool = True) -> str:
return path
def addonFolder(self) -> str:
- return self._ensureExists(os.path.join(self.base, "addons21"))
+ path = Path(os.environ.get("ANKI_ADDONS") or Path(self.base) / "addons21")
+ path.mkdir(parents=True, exist_ok=True)
+ return str(path.resolve())
def backupFolder(self) -> str:
return self._ensureExists(os.path.join(self.profileFolder(), "backups"))

View File

@ -0,0 +1,107 @@
{
lib,
symlinkJoin,
makeWrapper,
anki,
anki-utils,
writeTextDir,
ankiAddons ? [ ],
}:
/*
`ankiAddons`
: A set of Anki add-ons to be installed. Here's a an example:
~~~
pkgs.anki.withAddons [
# When the add-on is already available in nixpkgs
pkgs.ankiAddons.anki-connect
# When the add-on is not available in nixpkgs
(pkgs.anki-utils.buildAnkiAddon (finalAttrs: {
pname = "recolor";
version = "3.1";
src = pkgs.fetchFromGitHub {
owner = "AnKing-VIP";
repo = "AnkiRecolor";
rev = finalAttrs.version;
sparseCheckout = [ "src/addon" ];
hash = "sha256-28DJq2l9DP8O6OsbNQCZ0pm4S6CQ3Yz0Vfvlj+iQw8Y=";
};
sourceRoot = "source/src/addon";
}))
# When the add-on needs to be configured
pkgs.ankiAddons.passfail2.withConfig {
config = {
again_button_name = "not quite";
good_button_name = "excellent";
};
user_files = ./dir-to-be-merged-into-addon-user-files-dir;
};
]
~~~
The original `anki` executable will be wrapped so that it uses the addons from
`ankiAddons`.
This only works with Anki versions patched to support the `ANKI_ADDONS` environment
variable. `pkgs.anki` has this, but `pkgs.anki-bin` does not.
*/
let
defaultAddons = [
(anki-utils.buildAnkiAddon {
pname = "nixos";
version = "1.0";
src = writeTextDir "__init__.py" ''
import aqt
from aqt.qt import QMessageBox
import json
def addons_dialog_will_show(dialog: aqt.addons.AddonsDialog) -> None:
dialog.setEnabled(False)
QMessageBox.information(
dialog,
"NixOS Info",
("These add-ons are managed by NixOS.<br>"
"See <a href='https://github.com/NixOS/nixpkgs/tree/master/pkgs/games/anki/with-addons.nix'>"
"github.com/NixOS/nixpkgs/tree/master/pkgs/games/anki/with-addons.nix</a>")
)
def addon_tried_to_write_config(module: str, conf: dict) -> None:
message_box = QMessageBox(
QMessageBox.Icon.Warning,
"NixOS Info",
(f"The add-on module: \"{module}\" tried to update its config.<br>"
"See <a href='https://github.com/NixOS/nixpkgs/tree/master/pkgs/games/anki/with-addons.nix'>"
"github.com/NixOS/nixpkgs/tree/master/pkgs/games/anki/with-addons.nix</a>"
" for how to configure add-ons managed by NixOS.")
)
message_box.setDetailedText(json.dumps(conf))
message_box.exec()
aqt.gui_hooks.addons_dialog_will_show.append(addons_dialog_will_show)
aqt.mw.addonManager.writeConfig = addon_tried_to_write_config
'';
meta.maintainers = with lib.maintainers; [ junestepp ];
})
];
in
symlinkJoin {
inherit (anki) version;
pname = "${anki.pname}-with-addons";
paths = [ anki ];
nativeBuildInputs = [ makeWrapper ];
postBuild = ''
wrapProgram $out/bin/anki \
--set ANKI_ADDONS "${anki-utils.buildAnkiAddonsDir (ankiAddons ++ defaultAddons)}"
'';
meta = builtins.removeAttrs anki.meta [
"name"
"outputsToInstall"
"position"
];
}

View File

@ -14902,6 +14902,8 @@ with pkgs;
amoeba-data = callPackage ../games/amoeba/data.nix { };
anki = callPackage ../games/anki { };
anki-utils = callPackage ../games/anki/addons/anki-utils.nix { };
ankiAddons = recurseIntoAttrs (callPackage ../games/anki/addons { });
anki-bin = callPackage ../games/anki/bin.nix { };
anki-sync-server = callPackage ../games/anki/sync-server.nix { };