From 5d92c4f43dc5953283b94324cd6de26e9181d5b1 Mon Sep 17 00:00:00 2001 From: the-furry-hubofeverything Date: Sat, 21 Mar 2026 02:57:14 -0700 Subject: [PATCH] Init nix flake --- .gitignore | 7 ++ flake.lock | 61 +++++++++++ flake.nix | 25 +++++ nix/config-home-dir.diff | 13 +++ nix/deref-pycache.diff | 106 +++++++++++++++++++ nix/embed-pck.diff | 31 ++++++ nix/mediapipe-queue-mutex-delete.diff | 103 +++++++++++++++++++ nix/mediapipe.nix | 86 ++++++++++++++++ nix/package.nix | 142 ++++++++++++++++++++++++++ 9 files changed, 574 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/config-home-dir.diff create mode 100644 nix/deref-pycache.diff create mode 100644 nix/embed-pck.diff create mode 100644 nix/mediapipe-queue-mutex-delete.diff create mode 100644 nix/mediapipe.nix create mode 100644 nix/package.nix diff --git a/.gitignore b/.gitignore index 25926f6f..11b085f6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,10 @@ Build/godot.zip Build/godot_templates.tpz Saved/* + +# Ignore build outputs from performing a nix-build or `nix build` command +result +result-* + +# Ignore automatically generated direnv output +.direnv \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..752172bc --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1773628058, + "narHash": "sha256-hpXH0z3K9xv0fHaje136KY872VT2T5uwxtezlAskQgY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f8573b9c935cfaa162dd62cc9e75ae2db86f85df", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..1cbea329 --- /dev/null +++ b/flake.nix @@ -0,0 +1,25 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + inputs.self.submodules = true; + + outputs = { + nixpkgs, + flake-utils, + ... + }: + flake-utils.lib.eachSystem [ + "x86_64-linux" + ] ( + system: let + pkgs = nixpkgs.legacyPackages.${system}; + + # Matches semver compliant versioning in project.godot + regex = ".+config\/version=\"([[:alnum:]]+\.[[:alnum:]]+\.[[:alnum:]]-{0,1}[[:alnum:]]*+{0,1}[[:alnum:]]*)\".+"; + in { + formatter.${system} = pkgs.alejandra; + packages.default = pkgs.callPackage ./nix/package.nix {version = builtins.head (builtins.match regex (builtins.readFile ./project.godot));}; + packages.mediapipe = pkgs.callPackage ./nix/mediapipe.nix {}; + } + ); +} diff --git a/nix/config-home-dir.diff b/nix/config-home-dir.diff new file mode 100644 index 00000000..f589d02c --- /dev/null +++ b/nix/config-home-dir.diff @@ -0,0 +1,13 @@ +diff --git a/Core/Main.gd b/Core/Main.gd +index 37c7a00..89c100f 100644 +--- a/Core/Main.gd ++++ b/Core/Main.gd +@@ -740,7 +740,7 @@ func get_controller(): + static func get_saved_location() -> String: + if OS.has_feature("editor"): + return "res://Saved" +- return OS.get_executable_path().get_base_dir().path_join("Saved") ++ return OS.get_config_dir().path_join("snekstudio") + + ## Get the config directory. Will default to the user data directory unless + ## overridden by environment variable. diff --git a/nix/deref-pycache.diff b/nix/deref-pycache.diff new file mode 100644 index 00000000..e7ceb94d --- /dev/null +++ b/nix/deref-pycache.diff @@ -0,0 +1,106 @@ +diff --git a/addons/KiriPythonRPCWrapper/KiriPythonWrapperInstance.gd b/addons/KiriPythonRPCWrapper/KiriPythonWrapperInstance.gd +index 4f7943c..082648f 100644 +--- a/addons/KiriPythonRPCWrapper/KiriPythonWrapperInstance.gd ++++ b/addons/KiriPythonRPCWrapper/KiriPythonWrapperInstance.gd +@@ -66,18 +66,9 @@ func setup_python(force_unpack_extras : bool = false): + # Determine if we need to purge the cache based on what we're expecting to + # see version-wise vs what's actually represented there. + var needs_purge : bool = false +- var platform_status = _build_wrangler._get_platform_status() ++ # var platform_status = _build_wrangler._get_platform_status() + var cache_status = _build_wrangler.get_cache_status() + if cache_status.has("requirements_installed"): +- if cache_status["requirements_installed"] != platform_status["requirements"]: +- # Has the requirements field, and it's changed since we installed +- # last. +- needs_purge = true +- else: +- pass # No purge. Everything matches. +- else: +- # Doesn't have the requirements field. Possible indicator of a partial +- # install. + needs_purge = true + + # If we're running a build, let's see if the build's packaged version +@@ -94,22 +85,12 @@ func setup_python(force_unpack_extras : bool = false): + # No hash marker at all? Purge it just to be safe. + needs_purge = true + +- # Purge it if needed. +- if needs_purge: +- _build_wrangler.purge_cached_python() +- +- # Unpack base Python build. +- if _build_wrangler.unpack_python() == false: +- OS.alert("Unpacking Python failed!") +- return false +- + # Determine if we need to install whl files and unpack wrapper scripts. + var needs_setup = true +- platform_status = _build_wrangler._get_platform_status() ++ # platform_status = _build_wrangler._get_platform_status() + cache_status = _build_wrangler.get_cache_status() + if cache_status.has("requirements_installed"): +- if cache_status["requirements_installed"] == platform_status["requirements"]: +- needs_setup = false ++ needs_setup = false + + # Unpack Python wrapper, whl files, and project-specific scripts. + if needs_setup or force_unpack_extras or data_hash == null: +@@ -132,51 +113,15 @@ func setup_python(force_unpack_extras : bool = false): + var bytes : PackedByteArray = FileAccess.get_file_as_bytes(extra_script) + FileAccess.open(extraction_path, FileAccess.WRITE).store_buffer(bytes) + +- # Run pip to install packages from .whl files. +- var successfully_ran_pip_setup : bool = false +- if needs_setup: +- +- # Get a list of all the wheel files. +- var wheels_path : String = _build_wrangler._get_cache_path_godot().path_join("packaged_scripts/addons/KiriPythonRPCWrapper/Wheels") +- if DirAccess.dir_exists_absolute(wheels_path): +- var platform_wheels_path = wheels_path.path_join(KiriPythonBuildWrangler.get_host_os_name()) +- var wheel_list : PackedStringArray = DirAccess.get_files_at(platform_wheels_path) +- +- if len(wheel_list): +- +- var pip_args : PackedStringArray = [ +- "-m", "pip", "install", +- ] +- +- # Add every wheel file in the directory as an argument. +- for wheel in wheel_list: +- if wheel.ends_with(".whl"): +- pip_args.append(ProjectSettings.globalize_path(platform_wheels_path.path_join(wheel))) +- +- # Run pip. +- var output : Array = [] +- var pip_result = execute_python(pip_args, output, true, false) +- if pip_result != 0: +- OS.alert("Pip installation failed!\n" + "\n".join(output)) +- return false +- +- successfully_ran_pip_setup = true +- +- else: +- print("No wheel files detected. Skipping pip install.") +- else: +- print("Wheel directory does not exist. Skipping pip install.") +- + # FIXME: Delete wheel files? I don't think we need them anymore. If we made + # it this far, then we know we succeeded at the install. + + # Write success marker. +- if successfully_ran_pip_setup: +- cache_status = _build_wrangler.get_cache_status() +- cache_status["requirements_installed"] = platform_status["requirements"] +- if data_hash != null: +- cache_status["extra_data_hash"] = data_hash +- _build_wrangler.write_cache_status(cache_status) ++ cache_status = _build_wrangler.get_cache_status() ++ cache_status["requirements_installed"] = true ++ if data_hash != null: ++ cache_status["extra_data_hash"] = data_hash ++ _build_wrangler.write_cache_status(cache_status) + + return true + diff --git a/nix/embed-pck.diff b/nix/embed-pck.diff new file mode 100644 index 00000000..0e51f030 --- /dev/null +++ b/nix/embed-pck.diff @@ -0,0 +1,31 @@ +diff --git a/export_presets.cfg b/export_presets.cfg +index 1755037..83510c7 100644 +--- a/export_presets.cfg ++++ b/export_presets.cfg +@@ -21,7 +21,7 @@ script_export_mode=2 + custom_template/debug="" + custom_template/release="" + debug/export_console_wrapper=1 +-binary_format/embed_pck=false ++binary_format/embed_pck=true + texture_format/s3tc_bptc=true + texture_format/etc2_astc=false + binary_format/architecture="x86_64" +@@ -66,7 +66,7 @@ script_export_mode=2 + custom_template/debug="" + custom_template/release="" + debug/export_console_wrapper=1 +-binary_format/embed_pck=false ++binary_format/embed_pck=true + texture_format/s3tc_bptc=true + texture_format/etc2_astc=true + binary_format/architecture="arm64" +@@ -106,7 +106,7 @@ script_export_mode=2 + custom_template/debug="" + custom_template/release="" + debug/export_console_wrapper=1 +-binary_format/embed_pck=false ++binary_format/embed_pck=true + texture_format/s3tc_bptc=true + texture_format/etc2_astc=false + binary_format/architecture="x86_64" diff --git a/nix/mediapipe-queue-mutex-delete.diff b/nix/mediapipe-queue-mutex-delete.diff new file mode 100644 index 00000000..8ba81f49 --- /dev/null +++ b/nix/mediapipe-queue-mutex-delete.diff @@ -0,0 +1,103 @@ +diff --git a/Mods/MediaPipe/_tracker/Project/new_tracker.py b/Mods/MediaPipe/_tracker/Project/new_tracker.py +index 4bb067a..e210f44 100644 +--- a/Mods/MediaPipe/_tracker/Project/new_tracker.py ++++ b/Mods/MediaPipe/_tracker/Project/new_tracker.py +@@ -37,9 +37,6 @@ class MediaPipeTracker: + # We need these to avoid deadlocks. If we're queueing frames + # faster than they can process, we'll hit a deadlock in + # MediaPipe. +- self.frames_queued_face = 0 +- self.frames_queued_hands = 0 +- self.frames_queued_mutex = threading.Lock() + self.should_quit_threads = False + + # Open the socket immediately so we can start sending error +@@ -242,9 +239,6 @@ class MediaPipeTracker: + # move it into Godot. + self.last_blendshapes[shape.category_name] = shape.score # normalized + +- with self.frames_queued_mutex: +- self.frames_queued_face -= 1 +- + # FIXME: If we ever come back to it, finish this. + def _handle_result_pose( + self, +@@ -261,9 +255,6 @@ class MediaPipeTracker: + # self._write_log("HAND RESULTS: ", timestamp_ms) + # return + +- with self.frames_queued_mutex: +- self.frames_queued_hands -= 1 +- + # Check if hand count changed. Pause tracking for a moment if we + # did. + if self.last_hand_count != len(result.hand_landmarks): +@@ -549,28 +540,8 @@ class MediaPipeTracker: + if this_time <= last_timestamp_used: + continue + +- # Check to see if we have too many face tracking +- # frames queued. +- need_reset = False +- with self.frames_queued_mutex: +- if self.frames_queued_face > 5: +- need_reset = True +- else: +- self.frames_queued_face += 1 +- +- # This will get reset immediately if we detect +- # a face. Otherwise it'll be how many frames +- # since the last face detection. + self.time_since_last_face_detection += 1.0 +- +- # Reset if we have too face frames queued. Avoid a +- # deadlock. +- if need_reset: +- # Deadlock-avoidance. +- self.landmarker._runner.restart() +- self.frames_queued_face = 0 +- else: +- self.landmarker.detect_async(mp_image, this_time) ++ self.landmarker.detect_async(mp_image, this_time) + + # Hands + +@@ -585,23 +556,8 @@ class MediaPipeTracker: + if hand_landmarker_time_skew > 50: # FIXME: Make configurable (milliseconds) + self._last_hand_result_timestamp += this_time - self._last_hand_detect_timestamp + else: +- # Check to see if we have too many hand tracking +- # frames queued. +- need_reset = False +- with self.frames_queued_mutex: +- if self.frames_queued_face > 5: +- need_reset = True +- else: +- self.frames_queued_hands += 1 +- +- # If we do have too many frames queued, just reset +- # the tracker to avoid a deadlock. +- if need_reset: +- self.landmarker_hands._runner.restart() +- self.frames_queued_hands = 0 +- else: +- self.landmarker_hands.detect_async(mp_image, this_time) +- self._last_hand_detect_timestamp = this_time ++ self.landmarker_hands.detect_async(mp_image, this_time) ++ self._last_hand_detect_timestamp = this_time + + # Track the last timestamp because we have to keep + # these monotonically increasing and we can't send +@@ -644,12 +600,6 @@ class MediaPipeTracker: + + output_data_json = json.dumps(output_data, indent=4).encode("utf-8") + +- with self.frames_queued_mutex: +- status_packet_str = "Tracking data sending. (Queue: %2d hand, %2d face)" % (self.frames_queued_hands, self.frames_queued_face) +- +- # FIXME: This is too spammy. +- # self._write_log(status_packet_str) +- + # Output the packet. + if self.video_device_capture: + self._udp_socket.sendto(output_data_json, ("127.0.0.1", self.udp_port_number)) diff --git a/nix/mediapipe.nix b/nix/mediapipe.nix new file mode 100644 index 00000000..6d58e5bf --- /dev/null +++ b/nix/mediapipe.nix @@ -0,0 +1,86 @@ +# Contains code from https://github.com/NixOS/nixpkgs/pull/132275, proposed by emilytrau 🏳️‍⚧️ +{ + python3, + lib, + stdenv, + autoPatchelfHook, + fetchpatch, + zip, + unzip, + fetchPypi, +}: let + pname = "mediapipe"; + version = "0.10.33"; + format = "wheel"; +in + python3.pkgs.buildPythonPackage { + inherit pname version format; + + src = fetchPypi ({ + inherit pname version format; + python = "py3"; + dist = "py3"; + abi = "none"; + } + // { + "x86_64-linux" = { + platform = "manylinux_2_28_x86_64"; + sha256 = "sha256-QQ55YhXZ6yzx/dauy/QWq6tudKXupVcvS22pRljHZNk="; + }; + }.${ + stdenv.system + }); + + nativeBuildInputs = lib.optionals stdenv.hostPlatform.isLinux [unzip zip autoPatchelfHook]; + propagatedBuildInputs = with python3.pkgs; [ + absl-py + attrs + jax + jaxlib + numpy + sounddevice + # Working around nixos/nixpkgs#418899 + (protobuf4.overrideAttrs (oldAttrs: { + patches = + oldAttrs.patches + ++ [ + (fetchpatch { + name = "4-tests-missing-proto.patch"; + url = "https://raw.githubusercontent.com/kirelagin/nixpkgs/eb83e76382da36cb698e8f27b75f61a489a008b8/pkgs/development/python-modules/protobuf/4-tests-missing-proto.patch"; + hash = "sha256-TBuhlWXNQjRI8XmW74gZWKJ9DDWj1DjUVCh7rki5vY8="; + }) + (fetchpatch { + name = "4-tests-numpy-index.patch"; + url = "https://raw.githubusercontent.com/kirelagin/nixpkgs/eb83e76382da36cb698e8f27b75f61a489a008b8/pkgs/development/python-modules/protobuf/4-tests-numpy-index.patch"; + hash = "sha256-F9JJWwzdalSclC4gdILslukJE6Fh2i0EOVXFitIdScE="; + }) + ]; + })) + flatbuffers + matplotlib + opencv-contrib-python + ]; + + postPatch = '' + # Patch out requirement for static opencv and numpy so we can substitute it with the nix version + METADATA=mediapipe-${version}.dist-info/METADATA + unzip $src $METADATA + substituteInPlace $METADATA \ + --replace "Requires-Dist: opencv-contrib-python" "" + substituteInPlace $METADATA \ + --replace "Requires-Dist: numpy<2" "" + chmod +w dist/*.whl + zip -r dist/*.whl $METADATA + ''; + + pythonImportsCheck = [ + "mediapipe" + ]; + + meta = with lib; { + description = "Cross-platform, customizable ML solutions for live and streaming media"; + homepage = "https://ai.google.dev/edge/mediapipe"; + license = licenses.asl20; + platforms = [ "x86_64-linux" ]; + }; + } diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 00000000..97f5a39a --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,142 @@ +# Parts of https://github.com/puzzler995/katpkgs is referenced for this file. +# katpkgs is licensed under GPL v3. +{ + autoPatchelfHook, + writableTmpDirAsHomeHook, + stdenv, + version, + godotPackages_4_6, + fetchpatch, + lib, + callPackage, + python3, + libGL, + vulkan-loader, + libX11, + libXcursor, + libXext, + libXi, + libXrandr, + makeWrapper, + zip, + unzip, + fetchPypi, + makeDesktopItem, + copyDesktopItems, + wayland, +}: +stdenv.mkDerivation (finalAttrs: let + hostPlatform = stdenv.hostPlatform; + platform = "${hostPlatform.uname.system}-${hostPlatform.linuxArch}"; + mediapipe = callPackage ./mediapipe.nix { + inherit + python3 + lib + stdenv + autoPatchelfHook + fetchpatch + zip + unzip + fetchPypi + ; + }; + trackerPython = + python3.withPackages + (ps: [mediapipe ps.psutil ps.cv2-enumerate-cameras ps.numpy]); +in { + pname = "snekstudio"; + inherit version; + + src = ./..; + + nativeBuildInputs = [ + autoPatchelfHook + writableTmpDirAsHomeHook + copyDesktopItems + makeWrapper + godotPackages_4_6.godot + ]; + + runtimeDependencies = [ + trackerPython + wayland + libGL + vulkan-loader + libX11 + libXcursor + libXext + libXi + libXrandr + ]; + + patches = [ + ./deref-pycache.diff + ./embed-pck.diff + ./config-home-dir.diff + + # As mediapipe moved away from python _framework bindings for the facial landmark tracking + # since 0.10.32, frames_queued_mutex is no longer needed. + ./mediapipe-queue-mutex-delete.diff + ]; + + buildPhase = '' + runHook preBuild + + VERSION=${version} + + ln -s ${godotPackages_4_6.export-templates-bin} $HOME/.local + + echo ${trackerPython} + + mkdir -p $out/bin + sed -i "s#_build_wrangler.get_runtime_python_executable_system_path()#\"${trackerPython}/bin/python3\"#g" addons/KiriPythonRPCWrapper/KiriPythonWrapperInstance.gd + + godot --headless --import + + godot --headless --export-release "${platform}" Build/Builds/snekstudio + + runHook postBuild + ''; + + desktopItems = [ + ( + makeDesktopItem { + name = "snekstudio"; + desktopName = "SnekStudio"; + genericName = "VTuber Software"; + comment = "Open-source VTuber software"; + exec = "snekstudio"; + icon = "com.snekstudio.Snekstudio"; + terminal = false; + categories = ["Video" "AudioVideo"]; + startupNotify = true; + startupWMClass = "SnekStudio"; + } + ) + ]; + + installPhase = '' + runHook preInstall + + mkdir -p $out/lib/snekstudio + cd ./Build/Builds + + find Mods -type f -exec install -Dm 644 {} $out/lib/snekstudio/{} \; + find SampleModels -type f -exec install -Dm 644 {} $out/lib/snekstudio/{} \; + + install -D -m 755 snekstudio $out/bin/snekstudio + + wrapProgram $out/bin/snekstudio \ + --set SNEKSTUDIO_SAMPLE_PATH $out/lib/snekstudio/SampleModels \ + --set SNEKSTUDIO_MODS_PATHS $out/lib/snekstudio/Mods + + runHook postInstall + ''; + + meta = with lib; { + description = " Open-source VTuber software using Godot Engine!"; + homepage = "https://snekstudio.com/"; + license = licenses.gpl3; + platforms = ["x86_64-linux"]; + }; +})