diff --git a/modules/overlays.nix b/modules/overlays.nix new file mode 100644 index 0000000..47c4b17 --- /dev/null +++ b/modules/overlays.nix @@ -0,0 +1,7 @@ +{ + pkgs, + ... +}: +{ + nixpkgs.overlays = [ (import ../overlays/immich) ]; +} diff --git a/overlays/immich/default.nix b/overlays/immich/default.nix new file mode 100644 index 0000000..0fcb48f --- /dev/null +++ b/overlays/immich/default.nix @@ -0,0 +1,5 @@ +self: super: { + pkgs-src = { + immich = super.callPackage ./package.nix { }; + }; +} diff --git a/overlays/immich/package.nix b/overlays/immich/package.nix new file mode 100644 index 0000000..9eefd4d --- /dev/null +++ b/overlays/immich/package.nix @@ -0,0 +1,307 @@ +{ + lib, + stdenv, + buildNpmPackage, + fetchFromGitHub, + python3, + nodejs, + node-gyp, + runCommand, + nixosTests, + immich-machine-learning, + # build-time deps + glib, + pkg-config, + makeWrapper, + curl, + cacert, + unzip, + # runtime deps + cairo, + exiftool, + giflib, + jellyfin-ffmpeg, # Immich depends on the jellyfin customizations, see https://github.com/NixOS/nixpkgs/issues/351943 + imagemagick, + libjpeg, + libpng, + libraw, + libheif, + librsvg, + pango, + perl, + pixman, + vips, + buildPackages, + sourcesJSON ? ./sources.json, +}: +let + buildNpmPackage' = buildNpmPackage.override { inherit nodejs; }; + sources = lib.importJSON sourcesJSON; + inherit (sources) version; + + esbuild' = buildPackages.esbuild.override { + buildGoModule = + args: + buildPackages.buildGoModule ( + args + // rec { + version = "0.25.5"; + src = fetchFromGitHub { + owner = "evanw"; + repo = "esbuild"; + tag = "v${version}"; + hash = "sha256-jemGZkWmN1x2+ZzJ5cLp3MoXO0oDKjtZTmZS9Be/TDw="; + }; + vendorHash = "sha256-+BfxCyg0KkDQpHt/wycy/8CTG6YBA/VJvJFhhzUnSiQ="; + } + ); + }; + + buildLock = { + sources = + builtins.map + (p: { + name = p.pname; + inherit (p) version; + inherit (p.src) rev; + }) + [ + imagemagick + libheif + libraw + ]; + + packages = [ ]; + }; + + # The geodata website is not versioned, so we use the internet archive + geodata = + let + inherit (sources.components.geonames) timestamp; + date = + "${lib.substring 0 4 timestamp}-${lib.substring 4 2 timestamp}-${lib.substring 6 2 timestamp}T" + + "${lib.substring 8 2 timestamp}:${lib.substring 10 2 timestamp}:${lib.substring 12 2 timestamp}Z"; + in + runCommand "immich-geodata" + { + outputHash = sources.components.geonames.hash; + outputHashMode = "recursive"; + nativeBuildInputs = [ + cacert + curl + unzip + ]; + + meta.license = lib.licenses.cc-by-40; + } + '' + mkdir $out + url="https://web.archive.org/web/${timestamp}/http://download.geonames.org/export/dump" + curl -Lo ./cities500.zip "$url/cities500.zip" + curl -Lo $out/admin1CodesASCII.txt "$url/admin1CodesASCII.txt" + curl -Lo $out/admin2Codes.txt "$url/admin2Codes.txt" + curl -Lo $out/ne_10m_admin_0_countries.geojson \ + https://github.com/nvkelso/natural-earth-vector/raw/ca96624a56bd078437bca8184e78163e5039ad19/geojson/ne_10m_admin_0_countries.geojson + + unzip ./cities500.zip -d $out/ + echo "${date}" > $out/geodata-date.txt + ''; + + src = fetchFromGitHub { + owner = "immich-app"; + repo = "immich"; + tag = "v${version}"; + inherit (sources) hash; + }; + + openapi = buildNpmPackage' { + pname = "immich-openapi-sdk"; + inherit version; + src = "${src}/open-api/typescript-sdk"; + inherit (sources.components."open-api/typescript-sdk") npmDepsHash; + + installPhase = '' + runHook preInstall + + npm config delete cache + npm prune --omit=dev --omit=optional + + mkdir -p $out + mv package.json package-lock.json node_modules build $out/ + + runHook postInstall + ''; + }; + + web = buildNpmPackage' { + pname = "immich-web"; + inherit version src; + sourceRoot = "${src.name}/web"; + inherit (sources.components.web) npmDepsHash; + + # prePatch is needed because npmConfigHook is a postPatch + prePatch = '' + # some part of the build wants to use un-prefixed binaries. let them. + mkdir -p $TMP/bin + ln -s "$(type -p ${stdenv.cc.targetPrefix}pkg-config)" $TMP/bin/pkg-config || true + ln -s "$(type -p ${stdenv.cc.targetPrefix}c++filt)" $TMP/bin/c++filt || true + ln -s "$(type -p ${stdenv.cc.targetPrefix}readelf)" $TMP/bin/readelf || true + export PATH="$TMP/bin:$PATH" + ''; + + preBuild = '' + rm node_modules/@immich/sdk + ln -s ${openapi} node_modules/@immich/sdk + ''; + + env.npm_config_build_from_source = "true"; + + nativeBuildInputs = [ + pkg-config + ]; + + buildInputs = [ + # https://github.com/Automattic/node-canvas/blob/master/Readme.md#compiling + cairo + giflib + libjpeg + libpng + librsvg + pango + pixman + ]; + + installPhase = '' + runHook preInstall + + cp -r build $out + + runHook postInstall + ''; + }; + + vips' = vips.overrideAttrs (prev: { + mesonFlags = prev.mesonFlags ++ [ "-Dtiff=disabled" ]; + }); +in +buildNpmPackage' { + pname = "immich"; + inherit version; + src = "${src}/server"; + inherit (sources.components.server) npmDepsHash; + + # prePatch is needed because npmConfigHook is a postPatch + prePatch = '' + # pg_dumpall fails without database root access + # see https://github.com/immich-app/immich/issues/13971 + substituteInPlace src/services/backup.service.ts \ + --replace-fail '`/usr/lib/postgresql/''${databaseMajorVersion}/bin/pg_dumpall`' '`pg_dump`' + + # some part of the build wants to use un-prefixed binaries. let them. + mkdir -p $TMP/bin + ln -s "$(type -p ${stdenv.cc.targetPrefix}pkg-config)" $TMP/bin/pkg-config || true + ln -s "$(type -p ${stdenv.cc.targetPrefix}c++filt)" $TMP/bin/c++filt || true + ln -s "$(type -p ${stdenv.cc.targetPrefix}readelf)" $TMP/bin/readelf || true + export PATH="$TMP/bin:$PATH" + ''; + + nativeBuildInputs = [ + pkg-config + python3 + makeWrapper + glib + node-gyp # for building node_modules/sharp from source + ]; + + buildInputs = [ + jellyfin-ffmpeg + imagemagick + libraw + libheif + # https://github.com/Automattic/node-canvas/blob/master/Readme.md#compiling + cairo + giflib + libjpeg + libpng + librsvg + pango + pixman + # Required for sharp + vips' + ]; + + # Required because vips tries to write to the cache dir + makeCacheWritable = true; + + env.SHARP_FORCE_GLOBAL_LIBVIPS = 1; + env.ESBUILD_BINARY_PATH = lib.getExe esbuild'; + + preBuild = '' + # If exiftool-vendored.pl isn't found, exiftool is searched for on the PATH + rm -r node_modules/exiftool-vendored.* + ''; + + installPhase = '' + runHook preInstall + + npm config delete cache + npm prune --omit=dev + + # remove build artifacts that bloat the closure + rm -r node_modules/**/{*.target.mk,binding.Makefile,config.gypi,Makefile,Release/.deps} + + mkdir -p $out/build + mv package.json package-lock.json node_modules dist resources $out/ + ln -s ${web} $out/build/www + ln -s ${geodata} $out/build/geodata + + echo '${builtins.toJSON buildLock}' > $out/build/build-lock.json + + makeWrapper ${lib.getExe nodejs} $out/bin/admin-cli --add-flags $out/dist/main --add-flags cli + makeWrapper ${lib.getExe nodejs} $out/bin/server --add-flags $out/dist/main --chdir $out \ + --set IMMICH_BUILD_DATA $out/build --set NODE_ENV production \ + --suffix PATH : "${ + lib.makeBinPath [ + exiftool + jellyfin-ffmpeg + perl # exiftool-vendored checks for Perl even if exiftool comes from $PATH + ] + }" + + runHook postInstall + ''; + + passthru = { + tests = { + inherit (nixosTests) immich; + }; + + machine-learning = immich-machine-learning; + + inherit + src + sources + web + geodata + ; + updateScript = ./update.sh; + }; + + meta = { + changelog = "https://github.com/immich-app/immich/releases/tag/${src.tag}"; + description = "Self-hosted photo and video backup solution"; + homepage = "https://immich.app/"; + license = with lib.licenses; [ + agpl3Only + cc-by-40 # geonames + ]; + maintainers = with lib.maintainers; [ + dotlambda + jvanbruegge + Scrumplex + titaniumtown + ]; + platforms = lib.platforms.linux ++ lib.platforms.freebsd; + mainProgram = "server"; + }; +} diff --git a/overlays/immich/sources.json b/overlays/immich/sources.json new file mode 100644 index 0000000..e3896b5 --- /dev/null +++ b/overlays/immich/sources.json @@ -0,0 +1,26 @@ +{ + "version": "1.137.3", + "hash": "sha256-oKDIx63LayDWhd4uE16rqFWmmwwSWUsUvgZKsgE3KWg=", + "components": { + "cli": { + "npmDepsHash": "sha256-Cjk95tsQM89LkMq6H3B5WYdYrMi3hB6d1XpN2xhHv2U=", + "version": "2.2.77" + }, + "server": { + "npmDepsHash": "sha256-CvczIXE3Z3LwZezG7kbfJqg2fak2BRXTr0op1Jo1LIg=", + "version": "1.137.3" + }, + "web": { + "npmDepsHash": "sha256-PcNgD/JFt3221Qgi54XzQZNa53iw3BUe31DM8k+nz/4=", + "version": "1.137.3" + }, + "open-api/typescript-sdk": { + "npmDepsHash": "sha256-M4ahH6ZP0E3wEgK4VLqSsNjhMFNVTMeRFdzU9EO53vE=", + "version": "1.137.3" + }, + "geonames": { + "timestamp": "20250725064853", + "hash": "sha256-UzP8JapHTCpk5/6X5usLLXQUfqEOUgkq76CTIBZoz08=" + } + } +} diff --git a/overlays/immich/update.sh b/overlays/immich/update.sh new file mode 100644 index 0000000..1e11085 --- /dev/null +++ b/overlays/immich/update.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env nix-shell +#!nix-shell -i bash -p curl jq prefetch-npm-deps nix-prefetch-github coreutils ripgrep + +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +old_version=$(jq -r ".version" sources.json || echo -n "0.0.1") +version=$(curl -s "https://api.github.com/repos/immich-app/immich/releases/latest" | jq -r ".tag_name") +version="${version#v}" + +echo "Updating to $version" + +if [[ "$old_version" == "$version" ]]; then + echo "Already up to date!" + exit 0 +fi + +echo "Fetching src" +src_hash=$(nix-prefetch-github immich-app immich --rev "v${version}" | jq -r .hash) +upstream_src="https://raw.githubusercontent.com/immich-app/immich/v$version" + +sources_tmp="$(mktemp)" +cat < "$sources_tmp" +{ + "version": "$version", + "hash": "$src_hash", + "components": {} +} +EOF + +lock=$(mktemp) +for npm_component in cli server web "open-api/typescript-sdk"; do + echo "fetching $npm_component" + curl -s -o "$lock" "$upstream_src/$npm_component/package-lock.json" + hash=$(prefetch-npm-deps "$lock") + echo "$(jq --arg npm_component "$npm_component" \ + --arg hash "$hash" \ + --arg version "$(jq -r '.version' <"$lock")" \ + '.components += {($npm_component): {npmDepsHash: $hash, version: $version}}' \ + "$sources_tmp")" > "$sources_tmp" +done +rm "$lock" + +url="http://download.geonames.org/export/dump" +curl -s "http://web.archive.org/save/$url/cities500.zip" +curl -s "http://web.archive.org/save/$url/admin1CodesASCII.txt" +curl -s "http://web.archive.org/save/$url/admin1Codes.txt" +timestamp=$(curl -s \ + "http://archive.org/wayback/available?url=$url/cities500.zip" \ + | jq -r ".archived_snapshots.closest.timestamp") +echo "$(jq --arg timestamp "$timestamp" --arg hash "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" \ + '.components += {geonames: {timestamp: $timestamp, hash: $hash}}' \ + "$sources_tmp")" > "$sources_tmp" + +cp "$sources_tmp" sources.json +set +o pipefail +output="$(nix-build ../../../.. -A immich.geodata 2>&1 || true)" +set -o pipefail +hash=$(echo "$output" | rg 'got:\s+(sha256-.*)$' -or '$1') +echo "$(jq --arg hash "$hash" '.components.geonames.hash = $hash' "$sources_tmp")" > "$sources_tmp" + +mv "$sources_tmp" sources.json