diff --git a/2026-03-28-pr.md b/2026-03-28-pr.md new file mode 100644 index 000000000..c83774029 --- /dev/null +++ b/2026-03-28-pr.md @@ -0,0 +1,23 @@ +# PR Title + +Add Nix flake support and fix musl build configuration + +# PR Body + +## Summary + +- add a Nix flake with a default package, dev shell, portable musl build, and exported NixOS module +- add a NixOS module plus README and maintainer docs for running `etserver` from declarative config +- fix the non-vcpkg and musl build path by gating Catch2 on `BUILD_TESTING`, adding protobuf/absl/`utf8_range` link handling, and improving static `libunwind` linking +- make `format.sh` work in Nix environments by falling back to `clang-format` or `nix run` when `clang-format-18` is not present + +## Testing + +- `bash format.sh` +- `nix develop path:. --command bash -lc 'cmake -S . -B build -GNinja -DDISABLE_VCPKG=ON -DDISABLE_TELEMETRY=ON'` +- `nix develop path:. --command bash -lc 'cmake --build build -j"$(nproc)"'` +- `nix develop path:. --command bash -lc 'ctest --test-dir build --parallel "$(nproc)"'` + +## Notes + +- this keeps the Nix and musl work without reintroducing generated `build-musl/` artifacts or oversized binaries diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b650e3f3..966a19147 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,6 +113,8 @@ project(EternalTCP VERSION 6.2.11 LANGUAGES C CXX) include(CMakeFindDependencyMacro) +option(BUILD_TESTING "Build tests" ON) + # Add cmake script directory. list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") set(CMAKE_MODULE_PATH "${EXTERNAL_DIR}/sanitizers-cmake/cmake" ${CMAKE_MODULE_PATH}) @@ -131,7 +133,9 @@ find_package(Protobuf REQUIRED) find_package(Unwind) if(DISABLE_VCPKG) - add_subdirectory(${EXTERNAL_DIR}/Catch2) + if(BUILD_TESTING) + add_subdirectory(${EXTERNAL_DIR}/Catch2) + endif() add_subdirectory(${EXTERNAL_DIR}/cxxopts) add_subdirectory(${EXTERNAL_DIR}/cpp-httplib) add_subdirectory(${EXTERNAL_DIR}/json) @@ -144,7 +148,9 @@ if(DISABLE_VCPKG) ${EXTERNAL_DIR}/cxxopts/include ) else() - find_package(Catch2 CONFIG REQUIRED) + if(BUILD_TESTING) + find_package(Catch2 CONFIG REQUIRED) + endif() find_package(httplib CONFIG REQUIRED) find_package(cxxopts CONFIG REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) @@ -210,7 +216,6 @@ endif() option(CODE_COVERAGE "Enable code coverage" OFF) option(FUZZING "Enable builds for fuzz testing" OFF) -option(BUILD_TESTING "Build tests" ON) option(DISABLE_CRASH_LOG "Disable installing easylogging crash handler" OFF) option(INSTALL_BASH_COMPLETION "Install bash completion script" ON) option(INSTALL_ZSH_COMPLETION "Install zsh completion script" ON) @@ -261,8 +266,57 @@ set(PROTOBUF_LIBS protobuf::libprotobuf) if(Protobuf_VERSION VERSION_GREATER_EQUAL 4) find_package(absl REQUIRED) find_package(utf8_range CONFIG REQUIRED) + set(PROTOBUF_ABSL_LIBS + absl::absl_check + absl::absl_log + absl::algorithm + absl::base + absl::bind_front + absl::bits + absl::btree + absl::cleanup + absl::cord + absl::core_headers + absl::debugging + absl::die_if_null + absl::dynamic_annotations + absl::flags + absl::flat_hash_map + absl::flat_hash_set + absl::function_ref + absl::hash + absl::layout + absl::log_globals + absl::log_initialize + absl::log_internal_check_op + absl::log_severity + absl::memory + absl::node_hash_map + absl::node_hash_set + absl::optional + absl::random_distributions + absl::random_random + absl::span + absl::status + absl::statusor + absl::strings + absl::synchronization + absl::time + absl::utility + utf8_range::utf8_validity + utf8_range::utf8_range + ) - set(PROTOBUF_LIBS ${PROTOBUF_LIBS} absl::log_internal_check_op utf8_range::utf8_validity utf8_range::utf8_range) + if(Protobuf_USE_STATIC_LIBS) + set(PROTOBUF_LIBS + -Wl,--start-group + ${PROTOBUF_LIBS} + ${PROTOBUF_ABSL_LIBS} + -Wl,--end-group + ) + else() + set(PROTOBUF_LIBS ${PROTOBUF_LIBS} ${PROTOBUF_ABSL_LIBS}) + endif() endif() if(SELINUX_FOUND) @@ -322,7 +376,6 @@ else() resolv atomic stdc++fs - anl ) endif() diff --git a/NIXOS-readme.md b/NIXOS-readme.md new file mode 100644 index 000000000..e21a0e4ec --- /dev/null +++ b/NIXOS-readme.md @@ -0,0 +1,92 @@ +# EternalTerminal on NixOS + +This repo adds Nix support on top of the upstream CMake and Debian-style packaging. + +## Changes from base + +- `flake.nix` adds: + - `packages.default` and `packages.eternal-terminal` + - `devShells.default` + - `nixosModules.default` and `nixosModules.eternalTerminal` +- `default.nix` adds a NixOS module for running `etserver` +- `CMakeLists.txt` gates Catch2 behind `BUILD_TESTING=ON` so Nix package builds do not pull test-only dependencies + +## Nix behavior in this repo + +- Flake builds use `DISABLE_VCPKG=ON`, `DISABLE_SENTRY=ON`, `DISABLE_TELEMETRY=ON`, and `BUILD_TESTING=OFF`. +- The flake uses `inputs.self.submodules = true`, so consumers fetch the vendored submodules needed by the build. +- The package source is filtered with `lib.cleanSourceWith` so local `build/`, `cov_build/`, and `result/` directories do not leak into Nix builds. + +## Using it with Nix + +For a local checkout, use `path:.`: + +```bash +nix flake show path:. +nix develop path:. +nix build path:. --no-link +``` + +`nix develop` gives you a shell with the package inputs and the extra developer tools needed for normal CMake work on NixOS. + +## Using it with NixOS: module + package + +Common setup: import the NixOS module for the service, and add the package if you also want the client tools in `PATH`. + +The module manages the server side: + +- generates `/etc/et.cfg` +- starts `etserver` from the selected package +- opens the configured port when `openFirewall = true` + +Example flake-based NixOS configuration: + +```nix +{ + inputs.et.url = "github:MisterTea/EternalTerminal"; + + outputs = { nixpkgs, et, ... }: { + nixosConfigurations.my-host = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + et.nixosModules.default + ({ pkgs, ... }: { + environment.systemPackages = [ + et.packages.${pkgs.stdenv.hostPlatform.system}.default + ]; + + services.eternalTerminal = { + enable = true; + openFirewall = true; + port = 2022; + settings.Networking.bind_ip = "0.0.0.0"; + }; + }) + ]; + }; + }; +} +``` + +Notes: + +- The module runs the server, but it does not add the client tools to `PATH` by itself. Add the package to `environment.systemPackages` if you also want `et`, `htm`, and the other binaries available interactively. +- When imported through the flake, `services.eternalTerminal.package` defaults to the flake package. +- When importing `./default.nix` directly, set `services.eternalTerminal.package` yourself. + +## Module options + +- `services.eternalTerminal.enable` +- `services.eternalTerminal.package` +- `services.eternalTerminal.port` +- `services.eternalTerminal.openFirewall` +- `services.eternalTerminal.settings` + +`services.eternalTerminal.settings` is written as INI sections into `/etc/et.cfg`. `Networking.port` is always taken from `services.eternalTerminal.port`. + +## Service checks + +```bash +systemctl status eternal-terminal +journalctl -u eternal-terminal -b +``` diff --git a/README.md b/README.md index 1443bed4b..8a52f886d 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,34 @@ make sudo make install ``` +### NixOS + +If you use flakes, you can import the bundled NixOS module and let it manage +the package, `/etc/et.cfg`, and the `etserver` service: + +```nix +{ + inputs.et.url = "github:MisterTea/EternalTerminal"; + + outputs = { nixpkgs, et, ... }: { + nixosConfigurations.my-host = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + et.nixosModules.default + ({ ... }: { + services.eternalTerminal = { + enable = true; + openFirewall = true; + port = 2022; + settings.Networking.bind_ip = "0.0.0.0"; + }; + }) + ]; + }; + }; +} +``` + ### Windows Eternal Terminal works under WSL (Windows Subsystem for Linux). Follow the ubuntu instructions. @@ -148,7 +176,10 @@ You are ready to start using ET! ## Configuring -If you'd like to modify the server settings (e.g. to change the listening port), edit /etc/et.cfg. +If you'd like to modify the server settings (e.g. to change the listening +port), edit `/etc/et.cfg`. On NixOS, set `services.eternalTerminal.settings` +instead so the generated `/etc/et.cfg` stays in sync with your system +configuration. ## Using diff --git a/cmake/FindUnwind.cmake b/cmake/FindUnwind.cmake index eb0c0f98f..b72bfa794 100644 --- a/cmake/FindUnwind.cmake +++ b/cmake/FindUnwind.cmake @@ -12,6 +12,13 @@ if (PKG_CONFIG_FOUND) pkg_check_modules(PC_UNWIND QUIET libunwind) endif() +if (PC_UNWIND_FOUND) + set(Unwind_EXTRA_LIBRARIES ${PC_UNWIND_LINK_LIBRARIES}) + if (PC_UNWIND_STATIC_LINK_LIBRARIES) + set(Unwind_STATIC_EXTRA_LIBRARIES ${PC_UNWIND_STATIC_LINK_LIBRARIES}) + endif() +endif() + find_path (Unwind_INCLUDE_DIR NAMES unwind.h libunwind.h HINTS ${PC_UNWIND_INCLUDE_DIRS} @@ -89,13 +96,23 @@ find_package_handle_standard_args (Unwind REQUIRED_VARS Unwind_INCLUDE_DIR if (Unwind_FOUND) if (NOT TARGET unwind::unwind) + set(_Unwind_LINK_LIBRARIES ${Unwind_LIBRARY} ${Unwind_PLATFORM_LIBRARY}) + if (Unwind_LIBRARY MATCHES "\\.a$") + list(APPEND _Unwind_LINK_LIBRARIES ${Unwind_STATIC_EXTRA_LIBRARIES}) + if (UNIX) + list(APPEND _Unwind_LINK_LIBRARIES lzma) + endif() + else() + list(APPEND _Unwind_LINK_LIBRARIES ${Unwind_EXTRA_LIBRARIES}) + endif() + add_library (unwind::unwind INTERFACE IMPORTED) set_property (TARGET unwind::unwind PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${Unwind_INCLUDE_DIR} ) set_property (TARGET unwind::unwind PROPERTY - INTERFACE_LINK_LIBRARIES ${Unwind_LIBRARY} ${Unwind_PLATFORM_LIBRARY} + INTERFACE_LINK_LIBRARIES ${_Unwind_LINK_LIBRARIES} ) set_property (TARGET unwind::unwind PROPERTY IMPORTED_CONFIGURATIONS RELEASE diff --git a/default.nix b/default.nix new file mode 100644 index 000000000..103617a95 --- /dev/null +++ b/default.nix @@ -0,0 +1,96 @@ +{ + config, + lib, + pkgs, + package ? null, + ... +}: +let + cfg = config.services.eternalTerminal; + settingsFormat = pkgs.formats.ini { }; + defaultSettings = { + Debug = { + logdirectory = "/var/log/eternal-terminal"; + logsize = 20971520; + silent = 0; + telemetry = false; + verbose = 0; + }; + }; + mergedSettings = lib.recursiveUpdate defaultSettings cfg.settings; + effectiveSettings = lib.recursiveUpdate mergedSettings { + Networking.port = cfg.port; + }; + configFile = settingsFormat.generate "et.cfg" effectiveSettings; +in +{ + options.services.eternalTerminal = { + enable = lib.mkEnableOption "Eternal Terminal server"; + + package = lib.mkOption { + type = lib.types.nullOr lib.types.package; + default = package; + description = '' + Package that provides `etserver`. When this module is imported through + the flake, it defaults to that flake's package output. + ''; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 2022; + description = "TCP port for the Eternal Terminal server."; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Open the configured Eternal Terminal port in the firewall."; + }; + + settings = lib.mkOption { + type = settingsFormat.type; + default = { }; + example = { + Debug.serverfifo = "/run/etserver.fifo"; + Networking.bind_ip = "0.0.0.0"; + }; + description = '' + Extra `et.cfg` settings grouped by INI section. This is merged with the + module defaults, and `services.eternalTerminal.port` always wins for + `Networking.port`. + ''; + }; + }; + + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { + assertions = [ + { + assertion = cfg.package != null; + message = "services.eternalTerminal.package must be set when importing ./default.nix directly."; + } + ]; + + environment.etc."et.cfg".source = configFile; + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ]; + } + + (lib.mkIf (cfg.package != null) { + systemd.services."eternal-terminal" = { + description = "Eternal Terminal"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + ExecStart = "${cfg.package}/bin/etserver --cfgfile=/etc/et.cfg --logtostdout"; + LogsDirectory = "eternal-terminal"; + Restart = "on-failure"; + Type = "simple"; + }; + }; + }) + ] + ); +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..f19433472 --- /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": 1774386573, + "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", + "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 000000000..78111e25e --- /dev/null +++ b/flake.nix @@ -0,0 +1,158 @@ +{ + description = "EternalTerminal development shell, package, and NixOS module"; + + inputs = { + self.submodules = true; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + }: + let + nixosModule = + { + config, + lib, + pkgs, + ... + }: + import ./default.nix { + inherit config lib pkgs; + package = self.packages.${pkgs.system}.default; + }; + in + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { + inherit system; + }; + lib = pkgs.lib; + cleanedSource = lib.cleanSourceWith { + src = ./.; + filter = + path: type: + let + relPath = lib.removePrefix "${toString ./.}/" (toString path); + in + lib.cleanSourceFilter path type + && !( + relPath == "build" + || lib.hasPrefix "build/" relPath + || relPath == "cov_build" + || lib.hasPrefix "cov_build/" relPath + || relPath == "result" + || lib.hasPrefix "result/" relPath + ); + }; + mkEternalTerminal = + { + packageSet, + buildPackages ? packageSet.buildPackages, + }: + let + baseBuildInputs = [ + packageSet."abseil-cpp" + packageSet.libsodium + packageSet.libunwind + packageSet.openssl + packageSet.protobuf + packageSet.xz + packageSet.zlib + ]; + linuxExtras = lib.optionals packageSet.stdenv.hostPlatform.isLinux [ + packageSet.libselinux + packageSet.libutempter + ]; + in + packageSet.stdenv.mkDerivation rec { + pname = "eternal-terminal"; + version = "6.2.11"; + src = cleanedSource; + + strictDeps = true; + nativeBuildInputs = [ + buildPackages.cmake + buildPackages.ninja + buildPackages."pkg-config" + buildPackages.protobuf + ]; + buildInputs = baseBuildInputs ++ linuxExtras; + + cmakeFlags = [ + "-DBUILD_TESTING=OFF" + "-DDISABLE_SENTRY=ON" + "-DDISABLE_TELEMETRY=ON" + "-DDISABLE_VCPKG=ON" + "-DBASH_COMPLETION_COMPLETIONSDIR=${placeholder "out"}/share/bash-completion/completions" + "-DZSH_COMPLETIONS_DIR=${placeholder "out"}/share/zsh/site-functions" + ]; + + meta = with lib; { + description = "Remote terminal that reconnects without interrupting the session"; + homepage = "https://mistertea.github.io/EternalTerminal/"; + license = licenses.asl20; + mainProgram = "et"; + platforms = platforms.unix; + }; + }; + eternalTerminal = mkEternalTerminal { + packageSet = pkgs; + buildPackages = pkgs.buildPackages; + }; + packageBuildInputs = [ + pkgs."abseil-cpp" + pkgs.libsodium + pkgs.libunwind + pkgs.openssl + pkgs.protobuf + pkgs.zlib + ] + ++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [ + pkgs.libselinux + pkgs.libutempter + ]; + cmakePrefixPath = lib.makeSearchPathOutput "dev" "" packageBuildInputs; + in + { + packages = { + default = eternalTerminal; + "eternal-terminal" = eternalTerminal; + }; + + devShells = { + default = pkgs.mkShell { + inputsFrom = [ eternalTerminal ]; + + packages = [ + pkgs.autoconf + pkgs.automake + pkgs."bash-completion" + pkgs.curl + pkgs.git + pkgs.gnumake + pkgs.libtool + pkgs.llvmPackages_18.clang-tools + pkgs.unzip + pkgs.zip + ] + ++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.gdb ]; + + shellHook = '' + export OPENSSL_ROOT_DIR="${pkgs.openssl.dev}" + export CMAKE_PREFIX_PATH="${cmakePrefixPath}:$CMAKE_PREFIX_PATH" + ''; + }; + }; + } + ) + // { + nixosModules.default = nixosModule; + nixosModules.eternalTerminal = nixosModule; + }; +} diff --git a/format.sh b/format.sh index 94ce0906c..b81588324 100755 --- a/format.sh +++ b/format.sh @@ -1,2 +1,11 @@ #!/bin/bash -find ./src ./test -type f | grep "\.[hc]pp" | xargs clang-format-18 --style=Google -i + +if command -v clang-format-18 >/dev/null 2>&1; then + formatter=(clang-format-18) +elif command -v clang-format >/dev/null 2>&1; then + formatter=(clang-format) +else + formatter=(nix run 'github:NixOS/nixpkgs/nixos-unstable#llvmPackages_18.clang-tools' -- clang-format) +fi + +find ./src ./test -type f | grep "\.[hc]pp" | xargs "${formatter[@]}" --style=Google -i