diff --git a/.gitignore b/.gitignore index bd6dba61e34..7d490fdcdc0 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,5 @@ CmakeLists.txt provisioning/osxsierra.legally.ok *.gdb_history *vgcore.* -tmpfiles.init.setup +tmpfiles.init.setup +result* \ No newline at end of file diff --git a/docs/HowTos/nix/index.rst b/docs/HowTos/nix/index.rst new file mode 100644 index 00000000000..54327835dd9 --- /dev/null +++ b/docs/HowTos/nix/index.rst @@ -0,0 +1,617 @@ +.. _nix-packaging: + +Building PCP with Nix +===================== + +This guide documents how to build Performance Co-Pilot (PCP) using the Nix +package manager and the NixOS Linux distribution. + +.. important:: + + This documentation explains why ``flake.nix`` was created and the technical + decisions behind it, to help others maintain and update the Nix packaging in + the future. If/when a PCP package is added to nixpkgs, this document will + also serve as a reference for that Nix configuration. + +What is Nix? +------------ + +`Nix `_ is a purely functional package manager that provides +reproducible, declarative, and reliable software builds. Key features include: + +- **Reproducibility**: Builds are isolated and deterministic - the same inputs + always produce the same outputs, regardless of the host system state. +- **Atomic upgrades and rollbacks**: Packages are installed in isolation, + allowing instant rollback to previous versions. +- **Multiple versions**: Different versions of the same package can coexist + without conflicts. +- **NixOS**: A Linux distribution built entirely on Nix, where the entire + operating system configuration is declarative and reproducible. + +Nix stores all packages in ``/nix/store/`` with cryptographic hashes in the +path (e.g., ``/nix/store/abc123...-pcp-7.0.5/``), ensuring complete isolation +between package versions and configurations. + +For more information, visit: + +- `Nix website `_ +- `Nix manual `_ +- `Nixpkgs repository `_ + +.. contents:: Table of Contents + :depth: 3 + :local: + +Introduction +------------ + +PCP can be built using the Nix package manager, providing reproducible builds +and easy dependency management. This documentation covers: + +- Using the flake to build from source +- Available build options and feature flags +- Technical details of Nix-specific patches +- Known limitations and future improvements + +The Nix packaging work was initiated to bring PCP into the `nixpkgs +`_ repository, enabling NixOS users to easily +install and run PCP. + +Quick Start +----------- + +Building with Flakes (Recommended) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +From the PCP repository root:: + + # Build PCP + nix build + + # Run pminfo + nix run .#pcp -- pminfo --version + + # Enter a development shell + nix develop + +Feature Flags +------------- + +Default Features +^^^^^^^^^^^^^^^^ + +The following features are enabled by default: + +- **Core PCP tools and libraries**: pminfo, pmstat, pmrep, pmcd, etc. +- **Python3 and Perl language bindings** +- **Secure sockets**: SSL/TLS via OpenSSL +- **Service discovery**: via Avahi/mDNS +- **Transparent archive decompression**: via xz/lzma +- **Systemd integration**: unit files, tmpfiles, sysusers +- **Performance events**: via libpfm4 +- **BPF/BCC PMDAs**: kernel tracing via eBPF +- **SNMP PMDA**: network device monitoring +- **Device mapper metrics**: LVM thin/cache via lvm2 +- **RRDtool Perl bindings**: for RRD-based PMDAs + +Optional Features +^^^^^^^^^^^^^^^^^ + +These can be enabled by overriding the nixpkgs package: + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Flag + - Description + * - ``withQt = true`` + - Qt5 GUI tools (pmchart, pmview, etc.) + * - ``withAmdgpu = true`` + - AMD GPU metrics (requires libdrm) + * - ``withInfiniband = true`` + - Infiniband fabric metrics (requires rdma-core) + * - ``withSelinux = true`` + - SELinux policy support + * - ``withPythonExport = true`` + - pcp2arrow (pyarrow), pcp2xlsx (openpyxl) + * - ``withPythonDatabase = true`` + - pmdapostgresql, pmdamongodb + * - ``withPythonInfra = true`` + - pmdalibvirt (requires libvirt, lxml) + * - ``withPerlDatabase = true`` + - DBI for pmdaoracle, pmdamysql + * - ``withPerlNetwork = true`` + - Net::SNMP, Cache::Memcached, XML::LibXML + * - ``withPerlMisc = true`` + - Date::Manip, Spreadsheet::WriteExcel + +Example with overrides (nixpkgs only):: + + nix-build -A pcp.override { withQt = true; withPythonExport = true; } + +.. note:: + + The in-tree ``flake.nix`` provides a simplified build with default features + enabled (BPF, systemd, SNMP, etc.) but does not expose all optional flags. + For full customization, use the nixpkgs package with overrides. + +PMDAs Requiring Unavailable Dependencies +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some PMDAs require Python/Perl packages not yet available in nixpkgs: + +- **pcp2spark**: requires pyspark +- **pmdamssql**: requires pyodbc + unixODBC setup +- **pmdalio**: requires rtslib-fb +- **pmdahdb**: requires hdbcli/pyhdbcli (SAP HANA) +- **pmdaslurm**: requires Slurm::Sacctmgr Perl module + +Package Output Structure +------------------------ + +After building with ``nix build``, the ``result`` symlink points to the package +in the Nix store. The package follows a modified FHS-like structure: + +:: + + result/ + ├── bin/ # User-facing CLI tools + │ ├── pminfo # Query metric information + │ ├── pmstat # System performance overview + │ ├── pmrep # Customizable reporting + │ ├── pmlogger # Archive performance data + │ ├── pmie # Inference engine + │ ├── pcp2* # Export tools (json, xml, elasticsearch, etc.) + │ └── ... # ~70 tools total + │ + ├── lib/ # Shared libraries + │ ├── libpcp.so.* # Core PCP library + │ ├── libpcp_pmda.so.* # PMDA development library + │ ├── libpcp_web.so.* # Web/REST API support + │ ├── libpcp_gui.so.* # GUI toolkit integration + │ ├── python3.*/ # Python bindings (pcp module) + │ └── perl5/ # Perl bindings (PCP::PMDA, etc.) + │ + ├── libexec/pcp/ # Internal executables + │ ├── bin/ # pmcd, pmproxy, pmnsmerge, etc. + │ ├── pmdas/ # PMDA binaries and scripts + │ ├── pmns/ # PMNS source files (root_*) + │ └── services/ # Service control scripts + │ + ├── share/pcp/ # Shared data and config templates + │ ├── etc/ # Configuration files (moved from /etc) + │ │ ├── pcp.conf # Main PCP configuration + │ │ └── pcp/ # Per-service configs (pmcd, pmlogger, etc.) + │ ├── lib/ # Shell function libraries + │ ├── demos/ # Example code and tutorials + │ └── htop/ # pcp-htop configuration + │ + ├── var/lib/pcp/ # Variable data (static portions only) + │ ├── pmns/ # Combined PMNS root file + │ ├── pmdas/ # PMDA installation state + │ └── config/ # Runtime configuration templates + │ + └── include/ # C header files for development + └── pcp/ # pcp/*.h headers + +**Key differences from traditional installation:** + +- Configuration moved from ``/etc`` to ``share/pcp/etc`` (NixOS module + materializes to ``/etc`` at activation time) +- Runtime state directories (``/var/run``, ``/var/log``) removed (created + by systemd tmpfiles at runtime) +- All paths are absolute to the Nix store (``/nix/store/-pcp-x.y.z/``) + +Technical Details +----------------- + +This section documents the Nix-specific patches and workarounds required to +build PCP in the Nix sandbox environment. + +Why Nix Packaging Can Be Challenging +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Nix's approach to reproducible builds imposes constraints that differ +significantly from traditional Linux distributions. These constraints often +expose assumptions and hardcoded values in build systems that would otherwise +go unnoticed: + +**Non-standard Filesystem Hierarchy** + Nix does not follow the Filesystem Hierarchy Standard (FHS). Binaries, + libraries, and configuration files live in ``/nix/store/-/`` + rather than ``/usr/bin``, ``/usr/lib``, or ``/etc``. This breaks: + + - Hardcoded paths like ``/usr/bin/ar`` or ``/usr/lib/tmpfiles.d`` + - Scripts that assume tools are in ``PATH`` at standard locations + - Build systems that probe for dependencies in fixed paths + +**Sandboxed Builds** + Nix builds run in a sandbox with restricted filesystem access. The build + environment cannot write to locations like ``/tmp`` or ``/var/tmp`` - instead, + ``$TMPDIR`` points to an isolated temporary directory. This reveals: + + - Hardcoded temporary paths (e.g., ``/var/tmp`` in PCP's ``mk.rewrite`` scripts) + - Assumptions about writable system directories + - Network access attempts during builds (blocked by default) + +**Pure Build Environment** + Each build starts with a minimal, reproducible environment. There are no + implicit dependencies from the host system: + + - ``pkg-config`` returns paths inside Nix store (read-only for other packages) + - Compilers include hardening flags that may conflict with specialized targets + (e.g., BPF's ``-target bpf`` doesn't support ``-fstack-protector``) + - Environment variables like ``PATH`` contain only explicitly declared inputs + +**Static Analysis Benefits** + While these constraints require extra packaging work, they provide significant + benefits: + + - Exposes portability issues and implicit dependencies + - Forces explicit declaration of all build requirements + - Catches hardcoded paths that may break on other systems + - Results in packages that are more likely to build correctly on any platform + +The patches documented below address these Nix-specific challenges for PCP, +and many would benefit upstream by improving portability across all platforms. + +BPF Compilation +^^^^^^^^^^^^^^^ + +PCP's BPF PMDAs (pmdabpf, pmdabcc, pmdabpftrace) compile ``.bpf.c`` files to +BPF bytecode using clang with ``-target bpf``. Nix's wrapped clang adds +hardening flags that the BPF backend doesn't support:: + + clang: error: unsupported option '-fzero-call-used-regs=used-gpr' for target 'bpf' + clang: warning: ignoring '-fstack-protector-strong' option as it is not + currently supported for target 'bpf' + +**Solution**: + +1. Disable the unsupported hardening flag:: + + hardeningDisable = [ "zerocallusedregs" ]; + +2. Suppress stack protector warnings via ``BPF_CFLAGS``:: + + BPF_CFLAGS = "-fno-stack-protector -Wno-error=unused-command-line-argument"; + +3. Use ``lib.getExe`` to get the wrapped clang path:: + + CLANG = lib.getExe llvmPackages.clang; + +This approach was adapted from the ``xdp-tools`` package in nixpkgs, which +faces similar BPF compilation challenges. + +Nix Sandbox /var/tmp Fix +^^^^^^^^^^^^^^^^^^^^^^^^ + +The Nix build sandbox restricts filesystem access. Several PCP build scripts +hardcode ``/var/tmp`` for temporary files, causing errors:: + + ./mk.rewrite: line 12: /var/tmp/37183.c: No such file or directory + +**Affected scripts**: + +- ``src/pmdas/bind2/mk.rewrite`` +- ``src/pmdas/jbd2/mk.rewrite`` +- ``src/pmdas/linux/mk.rewrite`` +- ``src/pmdas/linux_proc/mk.rewrite`` +- ``src/bashrc/getargs`` +- ``src/libpcp/src/check-errorcodes`` +- ``src/libpcp3/src/check-errorcodes`` +- ``src/libpcp/doc/mk.cgraph`` +- ``src/pmlogcompress/check-optimize`` + +**Solution**: The patch file ``nix/patches/tmpdir-portability.patch`` replaces +``/var/tmp`` with ``${TMPDIR:-/tmp}`` in all affected scripts. This is portable: + +- On Nix, ``$TMPDIR`` is set to a writable sandbox directory +- On traditional systems, it falls back to ``/tmp`` + +This fix could be submitted upstream to improve portability across all build +environments. + +Systemd Integration Paths +^^^^^^^^^^^^^^^^^^^^^^^^^ + +PCP uses ``pkg-config`` to find systemd directories from ``systemd.pc``. On +NixOS, this returns paths inside the systemd store path (read-only):: + + mkdir: cannot create directory '/nix/store/...-systemd-.../lib/systemd/system': Permission denied + +Additionally, PCP's ``GNUmakefile`` has a hardcoded fallback to +``/usr/lib/tmpfiles.d``. + +**Solutions**: + +1. Override paths via environment variables:: + + SYSTEMD_SYSTEMUNITDIR = "${placeholder "out"}/lib/systemd/system"; + SYSTEMD_TMPFILESDIR = "${placeholder "out"}/lib/tmpfiles.d"; + SYSTEMD_SYSUSERSDIR = "${placeholder "out"}/lib/sysusers.d"; + +2. The patch file ``nix/patches/gnumakefile-nix-fixes.patch`` redirects the + tmpfiles installation from ``/usr/lib/tmpfiles.d`` to + ``$(PCP_SHARE_DIR)/tmpfiles.d``. + +Configure Script Fixes +^^^^^^^^^^^^^^^^^^^^^^ + +PCP's configure script has a hardcoded fallback for the ``ar`` archiver:: + + test -n "$AR" || AR="/usr/bin/ar" + +On NixOS, ``/usr/bin/ar`` doesn't exist. + +**Solution**: Export ``AR`` in ``preConfigure`` before configure runs:: + + preConfigure = '' + export AR="${stdenv.cc.bintools}/bin/ar" + ''; + +Since ``AR`` is already set when configure runs, the hardcoded fallback is never +reached. No patch is needed - the ``preConfigure`` export handles it completely. + +.. note:: + + The patch file ``nix/patches/configure-ar-portable.patch`` exists but is not + applied in ``flake.nix``. It changes the fallback to ``ar`` (PATH lookup) + and could be submitted upstream to improve portability for non-Nix builds. + +Install Ownership +^^^^^^^^^^^^^^^^^ + +PCP's ``GNUmakefile`` uses ``-o $(PCP_USER) -g $(PCP_GROUP)`` flags with +``install(1)`` to set file ownership. This requires root privileges and fails +in the Nix sandbox:: + + install: cannot change ownership: Operation not permitted + +**Solution**: The patch file ``nix/patches/gnumakefile-nix-fixes.patch`` removes +the ownership flags from install commands. The NixOS module (when created) will +handle proper ownership at activation time. + +Broken Symlinks Fix +^^^^^^^^^^^^^^^^^^^ + +PCP creates some symlinks with malformed targets containing double path +prefixes like ``/nix/store/nix/store/...``. This happens when the build system +prepends a path to an already-absolute path. + +**Affected symlinks** (after ``/etc`` is moved to ``share/pcp/etc/``): + +- ``share/pcp/etc/pcp/pmsearch/pmsearch.conf`` → should point to ``pmproxy/pmproxy.conf`` +- ``share/pcp/etc/pcp/pmseries/pmseries.conf`` → should point to ``pmproxy/pmproxy.conf`` +- ``share/pcp/etc/pcp/pmcd/rc.local`` → should point to ``libexec/pcp/services/local`` + +**Solution**: Fix in postInstall by removing and recreating with correct targets:: + + # Fix pmsearch/pmseries config symlinks + for broken_link in "$out/share/pcp/etc/pcp/pm"{search/pmsearch,series/pmseries}.conf; do + [[ -L "$broken_link" ]] && rm "$broken_link" && \ + ln -sf "$out/share/pcp/etc/pcp/pmproxy/pmproxy.conf" "$broken_link" + done + + # Fix pmcd/rc.local symlink + if [[ -L "$out/share/pcp/etc/pcp/pmcd/rc.local" ]]; then + rm "$out/share/pcp/etc/pcp/pmcd/rc.local" + ln -sf "$out/libexec/pcp/services/local" "$out/share/pcp/etc/pcp/pmcd/rc.local" + fi + +Patch Files +^^^^^^^^^^^ + +The Nix-specific fixes have been implemented as proper ``.patch`` files in +``nix/patches/`` rather than inline ``substituteInPlace`` calls: + +**Applied in flake.nix:** + +- ``gnumakefile-nix-fixes.patch`` - Ownership flags, tmpfiles path, qa exclusion +- ``tmpdir-portability.patch`` - Use ``${TMPDIR:-/tmp}`` instead of ``/var/tmp`` + +**Available but not applied** (handled by ``preConfigure`` instead): + +- ``configure-ar-portable.patch`` - Portable ar fallback (useful for upstream) + +Benefits of using patch files: + +- Patches fail loudly when they don't apply (easier version upgrades) +- Easier to submit upstream for review +- Clear documentation of what changes are needed +- Better for code review + +Only ``patchShebangs`` remains in ``postPatch`` as it requires Nix store paths. + +Package Outputs +--------------- + +The Nix package is split into multiple outputs to reduce closure size: + +.. list-table:: + :header-rows: 1 + :widths: 15 85 + + * - Output + - Contents + * - ``out`` + - Main binaries, libraries, PMDAs, examples, configuration templates + * - ``man`` + - Man pages (~15MB compressed) + * - ``doc`` + - Books, tutorials, programmer's guides + +Users who only need the binaries can install just ``out``, saving significant +disk space. + +Testing +------- + +QA Suite (Disabled) +^^^^^^^^^^^^^^^^^^^ + +The full QA test suite is disabled by default (``doCheck = false``) because: + +1. Tests require a running ``pmcd`` daemon +2. The QA suite has thousands of scripts that significantly slow builds + +The ``qa`` directory is patched out of ``SUBDIRS`` to prevent installation +entirely, avoiding slow shebang patching of test scripts. + +NixOS VM Test +^^^^^^^^^^^^^ + +A NixOS VM integration test is provided in ``nix/vm-test.nix`` to verify the +package works correctly in a real NixOS environment. The test: + +1. Boots a NixOS VM with PCP installed +2. Creates the required ``pcp`` user/group and runtime directories +3. Starts the ``pmcd`` daemon +4. Waits for pmcd to listen on port 44321 +5. Queries ``pminfo -f kernel.all.load`` to verify metrics collection works + +**Running the VM test**:: + + # Run via flake check (runs all checks) + nix flake check + + # Or build just the VM test + nix build .#checks.x86_64-linux.vm-test + + # Then to view the logs + nix log .#checks.x86_64-linux.vm-test + + # For aarch64 systems + nix build .#checks.aarch64-linux.vm-test + +Use ``-L`` to see test output in real-time:: + + nix build .#checks.x86_64-linux.vm-test -L + +**Example successful test output**:: + + === Checking listening ports === + machine: must succeed: ss -tlnp | grep -E '44321|pmcd' || echo 'No pmcd ports found' + machine: (finished: must succeed: ..., in 0.05 seconds) + LISTEN 0 5 0.0.0.0:44321 0.0.0.0:* users:(("pmcd",pid=947,fd=3)) + LISTEN 0 5 [::]:44321 [::]:* users:(("pmcd",pid=947,fd=4)) + + machine: waiting for TCP port 44321 on localhost + machine # Connection to localhost (::1) 44321 port [tcp/pmcd] succeeded! + machine: (finished: waiting for TCP port 44321 on localhost, in 0.05 seconds) + + machine: must succeed: PCP_CONF=/.../pcp.conf pminfo -f kernel.all.load + machine: (finished: must succeed: ... pminfo -f kernel.all.load, in 0.04 seconds) + + machine: must succeed: PCP_CONF=/.../pcp.conf pminfo -h localhost kernel.all.cpu.user + machine: (finished: must succeed: ... pminfo -h localhost kernel.all.cpu.user, in 0.03 seconds) + + === PCP VM test passed! === + (finished: run the VM test script, in 18.65 seconds) + test script finished in 18.69s + cleanup + kill machine (pid 9) + +**Running the VM interactively** (for debugging):: + + # Build the test driver + nix build .#checks.x86_64-linux.vm-test.driverInteractive + + # Run the interactive VM + ./result/bin/nixos-test-driver --interactive + + # Inside the Python REPL, you can run commands like: + # >>> machine.succeed("pminfo --version") + # >>> machine.shell_interact() # Get a shell in the VM + +**Test configuration** (from ``nix/vm-test.nix``): + +The VM is configured with: + +- PCP package installed via ``environment.systemPackages`` +- ``pcp`` user/group created as a system user +- Runtime directories (``/var/lib/pcp``, ``/var/log/pcp``, ``/run/pcp``) + created via systemd tmpfiles + +This test configuration serves as a reference for how to deploy PCP on NixOS +until a proper NixOS module is created. + +Future Improvements +------------------- + +Unbundle Vendored Dependencies +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +PCP vendors several libraries that already exist as packages in nixpkgs: + +- **inih**: INI file parser (``pkgs.inih``) +- **hiredis/hiredis-cluster**: Redis clients (``pkgs.hiredis``) +- **jsonsl**: JSON streaming parser +- **libbpf/bpftool**: BPF tooling (``pkgs.libbpf``, complex due to kernel version coupling) +- **htop source**: for pcp-htop (modified fork, ``pkgs.htop``) +- **BCC libbpf-tools**: BPF CO-RE tools (``pkgs.bcc``) + +Since nixpkgs already provides packages for most of these libraries, they could +be used directly instead of compiling the vendored copies. This would: + +- **Improve build performance**: Skip recompiling libraries that are already + built and cached in the Nix store +- **Reduce closure size**: Share libraries with other packages rather than + bundling duplicates +- **Automatically propagate security updates**: When nixpkgs updates a library, + PCP would automatically receive the fix +- **Follow nixpkgs best practices**: Vendoring is discouraged in favor of + explicit dependencies + +This requires upstream cooperation to ensure clean library boundaries and +configure flags to use system libraries. + +PMDA Extensibility +^^^^^^^^^^^^^^^^^^ + +Consider using ``lib.makeSearchPath`` to allow users to add custom PMDAs +without rebuilding the package, via ``PCP_PMDA_PATH`` or similar mechanism. + +NixOS Module +^^^^^^^^^^^^ + +A NixOS module (``services.pcp``) would provide: + +- Systemd service management for ``pmcd``, ``pmlogger``, ``pmproxy`` +- Declarative PMDA configuration +- Proper user/group creation +- Log rotation and retention policies +- Integration with ``services.prometheus`` for exporters + +Integrate NixOS Test +^^^^^^^^^^^^^^^^^^^^ + +The VM test currently exists as a standalone file. To fully integrate into +nixpkgs: + +1. Add to ``nixos/tests/all-tests.nix`` +2. Reference via ``passthru.tests = { inherit (nixosTests) pcp; };`` in the + package + +This would allow running ``nix-build -A nixosTests.pcp`` and ensure the test +runs automatically in nixpkgs CI. + +Summary +------- + +The Nix packaging of PCP provides: + +✅ Reproducible builds from source +✅ All core PMDAs and tools +✅ BPF/BCC kernel tracing support +✅ Python and Perl language bindings +✅ Systemd integration +✅ Split outputs for minimal installations + +Some features require enabling optional flags, and a few PMDAs need packages +not yet available in nixpkgs. + +For questions or contributions, please open an issue on the `PCP GitHub +repository `_ or the `nixpkgs +repository `_. diff --git a/docs/index.rst b/docs/index.rst index 661a114cbd7..92fd4c2db29 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,18 +15,20 @@ PCP is a feature-rich, mature, extensible, cross-platform toolkit supporting bot * :doc:`QG/QuickGuides` * :doc:`UAG/AboutUserAdministratorsGuide` * :doc:`PG/AboutProgrammersGuide` +* :doc:`HowTos/nix/index` * :doc:`HowTos/scaling/index` * `REST API Specification `_ .. toctree:: :caption: Guides :hidden: - + HowTos/installation/index QG/QuickReferenceGuide QG/QuickGuides UAG/AboutUserAdministratorsGuide PG/AboutProgrammersGuide + HowTos/nix/index HowTos/scaling/index REST API Specification diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000000..d8142e7a812 --- /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": 1767640445, + "narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-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 00000000000..ccdd7e726be --- /dev/null +++ b/flake.nix @@ -0,0 +1,310 @@ +# +# flake.nix - PCP Nix packaging +# +# See also: ./docs/HowTos/nix/index.rst +{ + description = "Performance Co-Pilot (PCP) - system performance monitoring toolkit"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + lib = pkgs.lib; + + pcp = pkgs.stdenv.mkDerivation rec { + pname = "pcp"; + version = "7.0.5"; + + src = ./.; + + outputs = [ + "out" + "man" + "doc" + ]; + + nativeBuildInputs = with pkgs; [ + autoconf + automake + pkg-config + bison + flex + which + perl + python3 + python3.pkgs.setuptools + ] ++ lib.optionals withBpf [ + llvmPackages.clang + llvmPackages.llvm + ]; + + buildInputs = with pkgs; [ + zlib + ncurses + readline + openssl + libuv + cyrus_sasl + inih + xz + python3 + perl + rrdtool + ] ++ lib.optionals pkgs.stdenv.isLinux [ + avahi + lvm2 + ] ++ lib.optionals withSystemd [ + systemd + ] ++ lib.optionals withPfm [ + libpfm + ] ++ lib.optionals withBpf [ + libbpf + bcc + elfutils + ] ++ lib.optionals withSnmp [ + net-snmp + ] ++ lib.optionals withPythonHttp [ + python3.pkgs.requests + ] ++ lib.optionals withPerlHttp [ + perlPackages.JSON + perlPackages.LWPUserAgent + ]; + + withSystemd = pkgs.stdenv.isLinux; + withPfm = pkgs.stdenv.isLinux; + withBpf = pkgs.stdenv.isLinux; + withSnmp = true; + withPythonHttp = true; + withPerlHttp = true; + + configureFlags = lib.concatLists [ + + [ + "--prefix=${placeholder "out"}" + "--sysconfdir=${placeholder "out"}/etc" + "--localstatedir=${placeholder "out"}/var" + "--with-rcdir=${placeholder "out"}/etc/init.d" + "--with-tmpdir=/tmp" + "--with-logdir=${placeholder "out"}/var/log/pcp" + "--with-rundir=/run/pcp" + ] + + [ + "--with-user=pcp" + "--with-group=pcp" + ] + + [ + "--with-make=make" + "--with-tar=tar" + "--with-python3=${lib.getExe pkgs.python3}" + ] + + [ + "--with-perl=yes" + "--with-threads=yes" + ] + + [ + "--with-secure-sockets=yes" + "--with-transparent-decompression=yes" + ] + + (if pkgs.stdenv.isLinux then [ "--with-discovery=yes" ] else [ "--with-discovery=no" ]) + + [ + "--with-dstat-symlink=no" + "--with-pmdamongodb=no" + "--with-pmdamysql=no" + "--with-pmdanutcracker=no" + "--with-qt=no" + "--with-infiniband=no" + "--with-selinux=no" + ] + + (if withSystemd then [ "--with-systemd=yes" ] else [ "--with-systemd=no" ]) + (if withPfm then [ "--with-perfevent=yes" ] else [ "--with-perfevent=no" ]) + ( + if withBpf then + [ + "--with-pmdabcc=yes" + "--with-pmdabpf=yes" + "--with-pmdabpftrace=yes" + ] + else + [ + "--with-pmdabcc=no" + "--with-pmdabpf=no" + "--with-pmdabpftrace=no" + ] + ) + + (if pkgs.stdenv.isLinux then [ "--with-devmapper=yes" ] else [ "--with-devmapper=no" ]) + + (if withSnmp then [ "--with-pmdasnmp=yes" ] else [ "--with-pmdasnmp=no" ]) + ]; + + + patches = [ + ./nix/patches/gnumakefile-nix-fixes.patch + ./nix/patches/tmpdir-portability.patch + ]; + + postPatch = '' + # Fix shebangs (can't be done as static patch - needs Nix store paths) + patchShebangs src build configure scripts man + ''; + + hardeningDisable = lib.optionals withBpf [ "zerocallusedregs" ]; + + BPF_CFLAGS = lib.optionalString withBpf "-fno-stack-protector -Wno-error=unused-command-line-argument"; + CLANG = lib.optionalString withBpf (lib.getExe pkgs.llvmPackages.clang); + + SYSTEMD_SYSTEMUNITDIR = lib.optionalString withSystemd "${placeholder "out"}/lib/systemd/system"; + SYSTEMD_TMPFILESDIR = lib.optionalString withSystemd "${placeholder "out"}/lib/tmpfiles.d"; + SYSTEMD_SYSUSERSDIR = lib.optionalString withSystemd "${placeholder "out"}/lib/sysusers.d"; + + preConfigure = '' + export AR="${pkgs.stdenv.cc.bintools}/bin/ar" + ''; + + postInstall = '' + # Build the combined PMNS root file + # The individual root_* files exist but pmcd needs a combined 'root' file + # Use pmnsmerge to combine all the root_* files into one + ( + cd $out/var/lib/pcp/pmns + export PCP_DIR=$out + export PCP_CONF=$out/etc/pcp.conf + . $out/etc/pcp.env + + # Merge all the root_* files into the combined root file + # Order matters: root_root first (base), then others + $out/libexec/pcp/bin/pmnsmerge -a \ + $out/libexec/pcp/pmns/root_root \ + $out/libexec/pcp/pmns/root_pmcd \ + $out/libexec/pcp/pmns/root_linux \ + $out/libexec/pcp/pmns/root_proc \ + $out/libexec/pcp/pmns/root_xfs \ + $out/libexec/pcp/pmns/root_jbd2 \ + $out/libexec/pcp/pmns/root_kvm \ + $out/libexec/pcp/pmns/root_mmv \ + $out/libexec/pcp/pmns/root_bpf \ + $out/libexec/pcp/pmns/root_pmproxy \ + root + ) + + # Remove runtime state directories + rm -rf $out/var/{run,log} $out/var/lib/pcp/tmp || true + + # Move vendor config to share + if [ -d "$out/etc" ]; then + mkdir -p $out/share/pcp/etc + mv $out/etc/* $out/share/pcp/etc/ + rmdir $out/etc || true + + # Fix paths in pcp.conf to point to new locations + substituteInPlace $out/share/pcp/etc/pcp.conf \ + --replace-quiet "$out/etc/pcp" "$out/share/pcp/etc/pcp" \ + --replace-quiet "$out/etc/sysconfig" "$out/share/pcp/etc/sysconfig" \ + --replace-quiet "PCP_ETC_DIR=$out/etc" "PCP_ETC_DIR=$out/share/pcp/etc" + + # Fix symlinks that pointed to /etc/pcp/... + find $out/var/lib/pcp -type l | while read link; do + target=$(readlink "$link") + if [[ "$target" == *"/etc/pcp/"* ]]; then + suffix="''${target#*/etc/pcp/}" + rm "$link" + ln -sf "$out/share/pcp/etc/pcp/$suffix" "$link" + fi + done + fi + + # Fix broken symlinks with double /nix/store prefix + # These occur when the build system prepends a path to an already-absolute path + for broken_link in "$out/share/pcp/etc/pcp/pm"{search/pmsearch,series/pmseries}.conf; do + [[ -L "$broken_link" ]] && rm "$broken_link" && \ + ln -sf "$out/share/pcp/etc/pcp/pmproxy/pmproxy.conf" "$broken_link" + done + + # Fix pmcd/rc.local symlink (points to libexec/pcp/services/local) + if [[ -L "$out/share/pcp/etc/pcp/pmcd/rc.local" ]]; then + rm "$out/share/pcp/etc/pcp/pmcd/rc.local" + ln -sf "$out/libexec/pcp/services/local" "$out/share/pcp/etc/pcp/pmcd/rc.local" + fi + + # Move man pages to $man output + if [ -d "$out/share/man" ]; then + mkdir -p $man/share + mv $out/share/man $man/share/ + fi + + # Move documentation to $doc output + for docdir in $out/share/doc/pcp*; do + if [ -d "$docdir" ]; then + mkdir -p $doc/share/doc + mv "$docdir" $doc/share/doc/ + fi + done + ''; + + doCheck = false; + enableParallelBuilding = true; + + meta = with lib; { + description = "Performance Co-Pilot - system performance monitoring toolkit"; + homepage = "https://pcp.io"; + license = licenses.gpl2Plus; + platforms = platforms.linux ++ platforms.darwin; + mainProgram = "pminfo"; + }; + }; + in + { + packages = { + default = pcp; + inherit pcp; + }; + + checks = lib.optionalAttrs pkgs.stdenv.isLinux { + vm-test = import ./nix/vm-test.nix { + inherit pkgs pcp; + }; + }; + + devShells.default = pkgs.mkShell { + inputsFrom = [ pcp ]; + packages = with pkgs; [ + gdb + jp2a + ] ++ lib.optionals pkgs.stdenv.isLinux [ + valgrind + ] ++ lib.optionals pkgs.stdenv.isDarwin [ + lldb + ]; + + shellHook = '' + # Display PCP logo on shell entry + if [[ -f ./images/pcpicon-light.png ]]; then + jp2a --colors ./images/pcpicon-light.png 2>/dev/null || true + fi + echo "PCP Development Shell" + echo "Run './configure --help' to see build options" + echo "Otherwise use 'nix build' to build the package" + ''; + }; + } + ); +} + diff --git a/nix/patches/configure-ar-portable.patch b/nix/patches/configure-ar-portable.patch new file mode 100644 index 00000000000..3da17ce3080 --- /dev/null +++ b/nix/patches/configure-ar-portable.patch @@ -0,0 +1,23 @@ +From: Nix Packaging +Subject: Use portable ar fallback in configure script + +The configure script has a hardcoded fallback to /usr/bin/ar which doesn't +exist on NixOS and other non-FHS systems. Change the fallback to just "ar" +which will be found via PATH. + +This is a more portable default that works on any system where ar is in PATH. +For Nix builds, AR is explicitly set in preConfigure for reproducibility, +so this fallback only affects non-Nix builds. + +--- a/configure ++++ b/configure +@@ -5947,7 +5947,7 @@ done + + test -n "$AR" && break + done +-test -n "$AR" || AR="/usr/bin/ar" ++test -n "$AR" || AR="ar" + + fi + if test ! -x "$AR"; then + diff --git a/nix/patches/gnumakefile-nix-fixes.patch b/nix/patches/gnumakefile-nix-fixes.patch new file mode 100644 index 00000000000..182b268a8f0 --- /dev/null +++ b/nix/patches/gnumakefile-nix-fixes.patch @@ -0,0 +1,57 @@ +From: Nix Packaging +Subject: Fix GNUmakefile for Nix build environment + +This patch makes three changes to GNUmakefile for Nix compatibility: + +1. Remove -o $(PCP_USER) -g $(PCP_GROUP) ownership flags from install commands. + The Nix sandbox cannot change file ownership (requires root). The NixOS + module handles proper ownership at activation time. + +2. Change the tmpfiles.d installation path from /usr/lib/tmpfiles.d to + $(PCP_SHARE_DIR)/tmpfiles.d. On NixOS, /usr/lib doesn't exist and we + need to install to the package output directory. + +3. Comment out 'SUBDIRS += qa' to skip testsuite installation. The QA suite + has thousands of scripts that significantly slow builds due to shebang + patching, and tests require a running pmcd daemon anyway. + +These changes improve portability and would benefit other non-FHS systems. + +--- a/GNUmakefile ++++ b/GNUmakefile +@@ -38,7 +38,8 @@ LDIRDIRT = pcp-[0-9]*.[0-9]*.[0-9]* pcp-*-[0-9]*.[0-9]*.[0-9]* + + SUBDIRS = vendor src + ifneq ($(TARGET_OS),mingw) +-SUBDIRS += qa ++# SUBDIRS += qa # Disabled for Nix - testsuite not needed in package ++# To run tests, build from source and use: cd qa && ./check + endif + SUBDIRS += man html images build debian + +@@ -80,9 +81,9 @@ endif + $(INSTALL) -m 755 -d $(PCP_PMNSADM_DIR) + $(INSTALL) -m 755 -d $(PCP_PMDASADM_DIR) + $(INSTALL) -m 755 -d $(PCP_SERVICES_DIR) +- $(INSTALL) -m 775 -o $(PCP_USER) -g $(PCP_GROUP) -d $(PCP_TMP_DIR) ++ $(INSTALL) -m 775 -d $(PCP_TMP_DIR) + ifeq "$(ENABLE_SYSTEMD)" "true" +- # this works if PCP_RUN_DIR (and friends) are within a tmpfs that ++ # This works if PCP_RUN_DIR (and friends) are within a tmpfs that + # is mounted empty on re-boot and managed by systemd-tmpfiles(8) +- $(INSTALL) -m 644 tmpfiles.init.setup /usr/lib/tmpfiles.d/pcp-reboot-init.conf ++ $(INSTALL) -m 644 tmpfiles.init.setup $(PCP_SHARE_DIR)/tmpfiles.d/pcp-reboot-init.conf + endif +@@ -102,8 +103,8 @@ endif + $(INSTALL) -m 755 -d $(PCP_VAR_DIR)/config + $(INSTALL) -m 755 -d $(PCP_VAR_DIR)/config/pmchart + $(INSTALL) -m 755 -d $(PCP_VAR_DIR)/config/pmieconf + $(INSTALL) -m 755 -d $(PCP_VAR_DIR)/config/pmlogconf +- $(INSTALL) -m 775 -o $(PCP_USER) -g $(PCP_GROUP) -d $(PCP_VAR_DIR)/config/pmda +- $(INSTALL) -m 775 -o $(PCP_USER) -g $(PCP_GROUP) -d $(PCP_LOG_DIR) ++ $(INSTALL) -m 775 -d $(PCP_VAR_DIR)/config/pmda ++ $(INSTALL) -m 775 -d $(PCP_LOG_DIR) + $(INSTALL) -m 755 -d $(PCP_PMDAS_DIR) + $(INSTALL) -m 755 -d $(PCP_DOC_DIR) + $(INSTALL) -m 755 -d $(PCP_DEMOS_DIR) + diff --git a/nix/patches/tmpdir-portability.patch b/nix/patches/tmpdir-portability.patch new file mode 100644 index 00000000000..91b0b1c567a --- /dev/null +++ b/nix/patches/tmpdir-portability.patch @@ -0,0 +1,123 @@ +From: Nix Packaging +Subject: Use portable TMPDIR instead of hardcoded /var/tmp + +Several PCP build scripts hardcode /var/tmp for temporary files. This fails +in the Nix sandbox which restricts filesystem access - /var/tmp is not +writable during Nix builds. + +This patch changes /var/tmp to ${TMPDIR:-/tmp} which: +- Uses $TMPDIR when set (Nix sets this to a writable sandbox directory) +- Falls back to /tmp on traditional systems where TMPDIR may not be set +- Is POSIX-compliant and improves portability across build environments + +Affected files (build-time scripts): +- src/pmdas/bind2/mk.rewrite +- src/pmdas/jbd2/mk.rewrite +- src/pmdas/linux/mk.rewrite +- src/pmdas/linux_proc/mk.rewrite +- src/bashrc/getargs +- src/libpcp/src/check-errorcodes +- src/libpcp3/src/check-errorcodes +- src/libpcp/doc/mk.cgraph +- src/pmlogcompress/check-optimize + +--- a/src/pmdas/bind2/mk.rewrite ++++ b/src/pmdas/bind2/mk.rewrite +@@ -11,7 +11,7 @@ + # and generate pmlogrewrite(1) rules to force the PMIDs to be correct. + # + +-tmp=/var/tmp/mk.rewrite-$$ ++tmp=${TMPDIR:-/tmp}/mk.rewrite-$$ + status=0 + trap "rm -f $tmp.*; exit \$status" 0 1 2 3 15 + +--- a/src/pmdas/jbd2/mk.rewrite ++++ b/src/pmdas/jbd2/mk.rewrite +@@ -6,7 +6,7 @@ + # platform dependent. + # + +-tmp=/var/tmp/$$ ++tmp=${TMPDIR:-/tmp}/$$ + trap "rm -f $tmp.*; exit 0" 0 1 2 3 15 + + cat <$tmp.c +--- a/src/pmdas/linux/mk.rewrite ++++ b/src/pmdas/linux/mk.rewrite +@@ -6,7 +6,7 @@ + # platform dependent. + # + +-tmp=/var/tmp/$$ ++tmp=${TMPDIR:-/tmp}/$$ + trap "rm -f $tmp.*; exit 0" 0 1 2 3 15 + + cat <$tmp.c +--- a/src/pmdas/linux_proc/mk.rewrite ++++ b/src/pmdas/linux_proc/mk.rewrite +@@ -6,7 +6,7 @@ + # platform dependent. + # + +-tmp=/var/tmp/$$ ++tmp=${TMPDIR:-/tmp}/$$ + trap "rm -f $tmp.*; exit 0" 0 1 2 3 15 + + cat <$tmp.c +--- a/src/bashrc/getargs ++++ b/src/bashrc/getargs +@@ -17,7 +17,7 @@ then + exit 1 + fi + +-tmp=/var/tmp/bashrc-getargs-$$ ++tmp=${TMPDIR:-/tmp}/bashrc-getargs-$$ + status=1 + trap "rm -f $tmp.*; exit \$status" 0 1 2 3 15 + +--- a/src/libpcp/src/check-errorcodes ++++ b/src/libpcp/src/check-errorcodes +@@ -6,7 +6,7 @@ + # Check error codes exposure in QA tests. + # + +-tmp=/var/tmp/check-errorcodes-$$ ++tmp=${TMPDIR:-/tmp}/check-errorcodes-$$ + rm -f $tmp.err + trap "sts=0; [ -f $tmp.err ] && sts=1; rm -f $tmp.*; exit \$sts" 0 1 2 3 15 + +--- a/src/libpcp3/src/check-errorcodes ++++ b/src/libpcp3/src/check-errorcodes +@@ -6,7 +6,7 @@ + # Check error codes exposure in QA tests. + # + +-tmp=/var/tmp/check-errorcodes-$$ ++tmp=${TMPDIR:-/tmp}/check-errorcodes-$$ + rm -f $tmp.err + trap "sts=0; [ -f $tmp.err ] && sts=1; rm -f $tmp.*; exit \$sts" 0 1 2 3 15 + +--- a/src/libpcp/doc/mk.cgraph ++++ b/src/libpcp/doc/mk.cgraph +@@ -5,7 +5,7 @@ + # Assumes libpcp source and cscope index are in ../src + # + +-tmp=/var/tmp/$$ ++tmp=${TMPDIR:-/tmp}/$$ + trap "rm -f $tmp.*; exit \$sts" 0 1 2 3 15 + + rm -f $tmp.* +--- a/src/pmlogcompress/check-optimize ++++ b/src/pmlogcompress/check-optimize +@@ -9,7 +9,7 @@ + # below there) + # + +-tmp=/var/tmp/check-optimize-$$ ++tmp=${TMPDIR:-/tmp}/check-optimize-$$ + here=`pwd` + + verbose=false + diff --git a/nix/vm-test.nix b/nix/vm-test.nix new file mode 100644 index 00000000000..750ecaf28eb --- /dev/null +++ b/nix/vm-test.nix @@ -0,0 +1,95 @@ +# NixOS VM test for Performance Co-Pilot (PCP) +# +# This test verifies: +# - PCP package builds and installs correctly +# - pmcd daemon can start and listen on port 44321 +# - Basic metrics can be queried via pminfo +# +# Run via flake: nix flake check +# Or standalone: nix build .#checks.x86_64-linux.vm-test +{ + pkgs, + pcp, +}: + +pkgs.testers.nixosTest { + name = "pcp-vm-test"; + + nodes.machine = + { pkgs, ... }: + { + environment.systemPackages = [ pcp ]; + + # Create pcp user/group required by pmcd + users.users.pcp = { + isSystemUser = true; + group = "pcp"; + description = "Performance Co-Pilot daemon user"; + }; + users.groups.pcp = { }; + + # Create required runtime directories + systemd.tmpfiles.rules = [ + "d /var/lib/pcp 0755 pcp pcp -" + "d /var/log/pcp 0755 pcp pcp -" + "d /run/pcp 0755 pcp pcp -" + ]; + }; + + testScript = '' + machine.wait_for_unit("multi-user.target") + + # Verify the package is installed and pminfo works + machine.succeed("pminfo --version") + + # Find the actual pcp package path by resolving pminfo symlink + # pminfo is at /nix/store/xxx-pcp-7.0.5/bin/pminfo + # We need to get the package root to find libexec/pcp/bin/pmcd + pminfo_real = machine.succeed("realpath $(which pminfo)").strip() + print(f"pminfo real path: {pminfo_real}") + + # Get package root (two levels up from bin/pminfo) + pkg_root = machine.succeed(f"dirname $(dirname {pminfo_real})").strip() + pmcd_path = f"{pkg_root}/libexec/pcp/bin/pmcd" + print(f"pmcd expected at: {pmcd_path}") + + # Verify pmcd exists + machine.succeed(f"test -x {pmcd_path}") + + # PCP_CONF points to the main configuration file + # After our postInstall, config is at share/pcp/etc/pcp.conf + pcp_conf = f"{pkg_root}/share/pcp/etc/pcp.conf" + + # Start pmcd daemon in background with PCP_CONF set + machine.succeed(f"PCP_CONF={pcp_conf} setsid {pmcd_path} -f > /tmp/pmcd.log 2>&1 &") + + # Give pmcd a moment to start or fail + import time + time.sleep(3) + + # Debug: check if pmcd is running and show log + print("=== Checking pmcd process ===") + ps_out = machine.succeed("ps aux | grep pmcd || true") + print(ps_out) + + print("=== pmcd log contents ===") + log_out = machine.succeed("cat /tmp/pmcd.log 2>/dev/null || echo 'No log file'") + print(log_out) + + print("=== Checking listening ports ===") + ports_out = machine.succeed("ss -tlnp | grep -E '44321|pmcd' || echo 'No pmcd ports found'") + print(ports_out) + + # Wait for pmcd to start listening on its default port + machine.wait_for_open_port(44321, timeout=30) + + # Query basic kernel metrics to verify pmcd is working + machine.succeed(f"PCP_CONF={pcp_conf} pminfo -f kernel.all.load") + + # Additional verification: check pmcd is responding + machine.succeed(f"PCP_CONF={pcp_conf} pminfo -h localhost kernel.all.cpu.user") + + print("=== PCP VM test passed! ===") + ''; +} +