From 0895e6727eb741e6a36a1eeff4fa83a0d1688f8a Mon Sep 17 00:00:00 2001 From: Ivan Mincik Date: Thu, 22 Aug 2024 12:59:25 +0200 Subject: [PATCH] nix: add nix packaging files for qgis --- flake.lock | 60 ++++++++ flake.nix | 77 +++++++++++ nix/package.nix | 53 ++++++++ nix/set-pyqt-package-dirs.patch | 49 +++++++ nix/unwrapped.nix | 234 ++++++++++++++++++++++++++++++++ 5 files changed, 473 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/package.nix create mode 100644 nix/set-pyqt-package-dirs.patch create mode 100644 nix/unwrapped.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000000..d5c4ef5e1c1 --- /dev/null +++ b/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1743550720, + "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "c621e8422220273271f52058f618c94e405bb0f5", + "type": "github" + }, + "original": { + "id": "flake-parts", + "type": "indirect" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1744868846, + "narHash": "sha256-5RJTdUHDmj12Qsv7XOhuospjAjATNiTMElplWnJE9Hs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ebe4301cbd8f81c4f8d3244b3632338bbeb6d49c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1743296961, + "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000000..e0def4579e7 --- /dev/null +++ b/flake.nix @@ -0,0 +1,77 @@ +{ + description = "QGIS"; + + nixConfig = { + # extra-substituters = [ "https://example.cachix.org" ]; + # extra-trusted-public-keys = [ "example.cachix.org-1:xxxx=" ]; + + # IFD is required for qgisVersion detection in nix/unwrapped.nix. + allow-import-from-derivation = true; + bash-prompt = "\\[\\033[1m\\][qgis-dev]\\[\\033\[m\\]\\040\\w >\\040"; + }; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + }; + + outputs = inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + + systems = [ "x86_64-linux" ]; + + perSystem = { config, self', inputs', pkgs, system, ... }: { + + packages = { + qgis = pkgs.libsForQt5.callPackage ./nix/package.nix { }; + }; + + devShells.default = + let + nixPatches = pkgs.lib.concatStringsSep " " self'.packages.qgis.passthru.unwrapped.patches; + + in + pkgs.mkShell { + inputsFrom = [ + self'.packages.qgis + self'.packages.qgis.passthru.unwrapped + ]; + + shellHook = '' + echo "Applying Nix patches ..." + for p in ${nixPatches}; do + echo "patch: $p" + patch --reverse --reject-file - --strip 1 < $p &> /dev/null || true + patch --strip 1 < $p + done + + export QT_PLUGIN_PATH="${pkgs.libsForQt5.qt5.qtbase}/${pkgs.libsForQt5.qt5.qtbase.qtPluginPrefix}" + export QT_QPA_PLATFORM_PLUGIN_PATH="${pkgs.libsForQt5.qt5.qtbase}/${pkgs.libsForQt5.qt5.qtbase.qtPluginPrefix}/platforms" + + + function dev-help { + + echo -e "\nWelcome to a QGIS development environment !" + echo "Build QGIS using following commands:" + echo + echo " 1. mkdir build && cd build" + echo " 2. cmake -G Ninja -D CMAKE_BUILD_TYPE=Debug -D CMAKE_INSTALL_PREFIX=\$(pwd)/app -DWITH_QTWEBKIT=OFF -DQT_PLUGINS_DIR=${pkgs.libsForQt5.qt5.qtbase}/${pkgs.libsForQt5.qt5.qtbase.qtPluginPrefix} .." + echo " 3. ninja" + echo " 4. ninja install" + echo + echo "Run tests:" + echo + echo "1. ninja test" + echo + echo "Note: run 'nix flake update' from time to time to update dependencies." + echo + echo "Run 'dev-help' to see this message again." + } + + dev-help + ''; + }; + }; + + flake = { }; + }; +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 00000000000..5ca3033e6da --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,53 @@ +{ makeWrapper +, nixosTests +, symlinkJoin + +, extraPythonPackages ? (ps: [ ]) + +, libsForQt5 + + # unwrapped package parameters +, withGrass ? false +, withServer ? false +, withWebKit ? false +}: + +let + qgis-unwrapped = libsForQt5.callPackage ./unwrapped.nix { + withGrass = withGrass; + withServer = withServer; + withWebKit = withWebKit; + }; +in +symlinkJoin rec { + + inherit (qgis-unwrapped) version; + name = "qgis-${version}"; + + paths = [ qgis-unwrapped ]; + + nativeBuildInputs = [ + makeWrapper + qgis-unwrapped.py.pkgs.wrapPython + ]; + + # Extend to add to the python environment of QGIS without rebuilding QGIS application. + pythonInputs = qgis-unwrapped.pythonBuildInputs ++ (extraPythonPackages qgis-unwrapped.py.pkgs); + + postBuild = '' + buildPythonPath "$pythonInputs" + + for program in $out/bin/*; do + wrapProgram $program \ + --prefix PATH : $program_PATH \ + --set PYTHONPATH $program_PYTHONPATH + done + ''; + + passthru = { + unwrapped = qgis-unwrapped; + tests.qgis = nixosTests.qgis; + }; + + meta = qgis-unwrapped.meta; +} diff --git a/nix/set-pyqt-package-dirs.patch b/nix/set-pyqt-package-dirs.patch new file mode 100644 index 00000000000..a1771d9f03d --- /dev/null +++ b/nix/set-pyqt-package-dirs.patch @@ -0,0 +1,49 @@ +diff --git a/cmake/FindPyQt5.cmake b/cmake/FindPyQt5.cmake +index b51fd0075e..87ee317e05 100644 +--- a/cmake/FindPyQt5.cmake ++++ b/cmake/FindPyQt5.cmake +@@ -25,7 +25,7 @@ ELSE(EXISTS PYQT5_VERSION_STR) + IF(SIP_BUILD_EXECUTABLE) + # SIP >= 5.0 path + +- FILE(GLOB _pyqt5_metadata "${Python_SITEARCH}/PyQt5-*.dist-info/METADATA") ++ FILE(GLOB _pyqt5_metadata "@pyQt5PackageDir@/PyQt5-*.dist-info/METADATA") + IF(_pyqt5_metadata) + FILE(READ ${_pyqt5_metadata} _pyqt5_metadata_contents) + STRING(REGEX REPLACE ".*\nVersion: ([^\n]+).*$" "\\1" PYQT5_VERSION_STR ${_pyqt5_metadata_contents}) + +diff --git a/cmake/FindQsci.cmake b/cmake/FindQsci.cmake +index 69e41c1fe9..5456c3d59b 100644 +--- a/cmake/FindQsci.cmake ++++ b/cmake/FindQsci.cmake +@@ -24,7 +24,7 @@ ELSE(QSCI_MOD_VERSION_STR) + IF(SIP_BUILD_EXECUTABLE) + # SIP >= 5.0 path + +- FILE(GLOB _qsci_metadata "${Python_SITEARCH}/QScintilla*.dist-info/METADATA") ++ FILE(GLOB _qsci_metadata "@qsciPackageDir@/QScintilla*.dist-info/METADATA") + IF(_qsci_metadata) + FILE(READ ${_qsci_metadata} _qsci_metadata_contents) + STRING(REGEX REPLACE ".*\nVersion: ([^\n]+).*$" "\\1" QSCI_MOD_VERSION_STR ${_qsci_metadata_contents}) +@@ -33,7 +33,7 @@ ELSE(QSCI_MOD_VERSION_STR) + ENDIF(_qsci_metadata) + + IF(QSCI_MOD_VERSION_STR) +- SET(QSCI_SIP_DIR "${PYQT_SIP_DIR}") ++ SET(QSCI_SIP_DIR "@qsciPackageDir@/PyQt5/bindings") + SET(QSCI_FOUND TRUE) + ENDIF(QSCI_MOD_VERSION_STR) + +diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt +index 4cd19c3af4..668cc6a5e6 100644 +--- a/python/CMakeLists.txt ++++ b/python/CMakeLists.txt +@@ -212,7 +212,7 @@ if (WITH_GUI) + install(FILES ${QGIS_PYTHON_OUTPUT_DIRECTORY}/_gui.pyi DESTINATION ${QGIS_PYTHON_DIR}) + endif() + if(QSCI_SIP_DIR) +- set(SIP_EXTRA_OPTIONS ${SIP_EXTRA_OPTIONS} -I ${QSCI_SIP_DIR}) ++ set(SIP_BUILD_EXTRA_OPTIONS ${SIP_BUILD_EXTRA_OPTIONS} --include-dir=${QSCI_SIP_DIR}) + else() + message(STATUS "Qsci sip file not found - disabling bindings for derived classes") + set(SIP_DISABLE_FEATURES ${SIP_DISABLE_FEATURES} HAVE_QSCI_SIP) diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix new file mode 100644 index 00000000000..201cc7de12f --- /dev/null +++ b/nix/unwrapped.nix @@ -0,0 +1,234 @@ +{ lib +, makeWrapper +, mkDerivation +, replaceVars +, runCommand +, wrapGAppsHook3 +, wrapQtAppsHook + +, withGrass +, withServer +, withWebKit + +, bison +, cmake +, draco +, exiv2 +, fcgi +, flex +, geos +, grass +, gsl +, hdf5 +, libspatialindex +, libspatialite +, libzip +, netcdf +, ninja +, openssl +, pdal +, libpq +, proj +, protobuf +, python3 +, qca-qt5 +, qscintilla +, qt3d +, qtbase +, qtkeychain +, qtlocation +, qtmultimedia +, qtsensors +, qtserialport +, qtwebkit +, qtxmlpatterns +, qwt +, sqlite +, txt2tags +, zstd +}: + +let + versionSourceFiles = lib.fileset.toSource { + root = ../.; + fileset = ../CMakeLists.txt; + }; + + qgisSourceFiles = + lib.fileset.difference + (lib.fileset.gitTracked ../.) + (lib.fileset.unions [ + # excluded files + ./. + ../flake.nix + ../flake.lock + ]); + + # Version parsing taken from + # https://github.com/qgis/QGIS/blob/1f0328cff6a8b4cf8a4f8d44a4304b9d9706aa72/rpm/buildrpms.sh#L118 + qgisVersion = + lib.replaceStrings [ "\n" ] [ "" ] + (lib.readFile ( + runCommand "qgis-version" { } '' + major=$(grep -ie 'SET(CPACK_PACKAGE_VERSION_MAJOR' ${versionSourceFiles}/CMakeLists.txt | + sed -r 's/.*\"([0-9]+)\".*/\1/g') + minor=$(grep -ie 'SET(CPACK_PACKAGE_VERSION_MINOR' ${versionSourceFiles}/CMakeLists.txt | + sed -r 's/.*\"([0-9]+)\".*/\1/g') + patch=$(grep -ie 'SET(CPACK_PACKAGE_VERSION_PATCH' ${versionSourceFiles}/CMakeLists.txt | + sed -r 's/.*\"([0-9]+)\".*/\1/g') + + version=$major.$minor.$patch + echo $version > $out + '' + )); + + py = python3.override { + self = py; + packageOverrides = self: super: { + pyqt5 = super.pyqt5.override { + withLocation = true; + withSerialPort = true; + }; + }; + }; + + pythonBuildInputs = with py.pkgs; [ + chardet + gdal + jinja2 + numpy + owslib + psycopg2 + pygments + pyqt5 + pyqt-builder + python-dateutil + pytz + pyyaml + qscintilla-qt5 + requests + setuptools + sip + six + urllib3 + ]; + +in + +# Print the list of included source files +# lib.fileset.trace qgisSourceFiles +mkDerivation +{ + pname = "qgis-unwrapped"; + version = qgisVersion; # this is a "Import from derivation (IFD)" ! + src = lib.fileset.toSource { + root = ../.; + fileset = qgisSourceFiles; + }; + + nativeBuildInputs = [ + makeWrapper + wrapGAppsHook3 + wrapQtAppsHook + + bison + cmake + flex + ninja + ]; + + buildInputs = [ + draco + exiv2 + fcgi + geos + gsl + hdf5 + libspatialindex + libspatialite + libzip + netcdf + openssl + pdal + libpq + proj + protobuf + qca-qt5 + qscintilla + qt3d + qtbase + qtkeychain + qtlocation + qtmultimedia + qtsensors + qtserialport + qtxmlpatterns + qwt + sqlite + txt2tags + zstd + ] ++ lib.optional withGrass grass + ++ lib.optional withWebKit qtwebkit + ++ pythonBuildInputs; + + patches = [ + (replaceVars ./set-pyqt-package-dirs.patch { + pyQt5PackageDir = "${py.pkgs.pyqt5}/${py.pkgs.python.sitePackages}"; + qsciPackageDir = "${py.pkgs.qscintilla-qt5}/${py.pkgs.python.sitePackages}"; + }) + ]; + + # Add path to Qt platform plugins + # (offscreen is needed by "${APIS_SRC_DIR}/generate_console_pap.py") + env.QT_QPA_PLATFORM_PLUGIN_PATH = "${qtbase}/${qtbase.qtPluginPrefix}/platforms"; + + cmakeFlags = [ + "-DWITH_3D=True" + "-DWITH_PDAL=True" + "-DENABLE_TESTS=False" + "-DQT_PLUGINS_DIR=${qtbase}/${qtbase.qtPluginPrefix}" + ] ++ lib.optional (!withWebKit) "-DWITH_QTWEBKIT=OFF" + ++ lib.optional withServer [ + "-DWITH_SERVER=True" + "-DQGIS_CGIBIN_SUBDIR=${placeholder "out"}/lib/cgi-bin" + ] + ++ lib.optional withGrass ( + let + gmajor = lib.versions.major grass.version; + gminor = lib.versions.minor grass.version; + in + "-DGRASS_PREFIX${gmajor}=${grass}/grass${gmajor}${gminor}" + ); + + qtWrapperArgs = [ + "--set QT_QPA_PLATFORM_PLUGIN_PATH ${qtbase}/${qtbase.qtPluginPrefix}/platforms" + ]; + + dontWrapGApps = true; # wrapper params passed below + + postFixup = lib.optionalString withGrass '' + # GRASS has to be availble on the command line even though we baked in + # the path at build time using GRASS_PREFIX. + # Using wrapGAppsHook also prevents file dialogs from crashing the program + # on non-NixOS. + for program in $out/bin/*; do + wrapProgram $program \ + "''${gappsWrapperArgs[@]}" \ + --prefix PATH : ${lib.makeBinPath [ grass ]} + done + ''; + + passthru = { + inherit pythonBuildInputs; + inherit py; + }; + + meta = with lib; { + description = "Free and Open Source Geographic Information System"; + homepage = "https://www.qgis.org"; + license = licenses.gpl2Plus; + maintainers = with maintainers; teams.geospatial.members; + platforms = with platforms; linux; + mainProgram = "qgis"; + }; +}